Merge commit '723c7817b6565807524e179119ea064eec86392f'

This commit is contained in:
2026-01-15 19:25:06 +08:00
29 changed files with 355 additions and 135 deletions

View File

@@ -1,12 +1,11 @@
from pydantic_settings import BaseSettings,SettingsConfigDict
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env" , env_prefix="WNZS_")
model_config = SettingsConfigDict(env_file=".env", env_prefix="WNZS_")
PGSQL: str = ""
WECOM_PROXY: str = ""
WECOM_CORPID: str = ""
WECOM_CORPSECRET: str = ""
WECOM_APP_TOKEN: str = ""
WECOM_APP_ENCODING_AES_KEY: str = ""

View File

@@ -1,7 +1,8 @@
import http
from fastapi import FastAPI, HTTPException, Request
from pydantic import ValidationError
from fastapi.responses import JSONResponse
from pydantic import ValidationError
from uvicorn.server import logger
exceptions = [Exception, HTTPException, ValidationError]

View File

@@ -3,6 +3,21 @@ from contextlib import asynccontextmanager
from fastapi import FastAPI
from uvicorn.server import logger
async def test_init():
from service.sync.department import sync_department, check_department_datebase
from service.sync.employee import sync_department_user, check_employee_datebase
if not check_department_datebase():
logger.info("[数据库] 开始同步部门 📦")
await sync_department()
logger.info("[数据库] 同步部门完成 📦")
if not check_employee_datebase():
logger.info("[数据库] 开始同步员工 📦")
await sync_department_user()
logger.info("[数据库] 同步员工完成 📦")
def init_database():
from model import create_db_and_tables
@@ -10,12 +25,14 @@ def init_database():
create_db_and_tables()
logger.info("[数据库] 数据库初始化完成 ✅")
def init_scheduler(app : FastAPI):
def init_scheduler(app: FastAPI):
from scheduler import init_scheduler_router
logger.info("[定时任务] 初始化定时任务 📦")
init_scheduler_router(app)
logger.info("[定时任务] 定时任务初始化完成 ✅")
def active_config():
logger.info(f"[激活配置] 加载配置 ⚙️")
@@ -29,12 +46,15 @@ def import_router(app: FastAPI):
app.include_router(router)
logger.info(f"[导入路由] 路由导入完成 ✅")
async def import_mcp_server(app: FastAPI):
logger.info(f"[导入MCP] 开始导入MCP 🛣️")
from mcps import create_mcp_app
app.mount("/app" , await create_mcp_app())
app.mount("/app", await create_mcp_app())
logger.info(f"[导入MCP] MCP导入完成 ✅")
@asynccontextmanager
async def lifespan(app: FastAPI):
logger.info(f"[生命周期] 应用启动 🚀")
@@ -43,5 +63,6 @@ async def lifespan(app: FastAPI):
import_router(app)
init_scheduler(app)
await import_mcp_server(app)
await test_init()
yield
logger.info(f"[生命周期] 应用关闭 🔧✅")

View File

@@ -4,7 +4,6 @@ from handler.exception import install as exception_install
from lifespan import lifespan
from mcps import create_mcp_app
app = FastAPI(lifespan=lifespan)
exception_install(app)
@@ -12,4 +11,4 @@ exception_install(app)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app=app, port=8000)
uvicorn.run(app=app, port=8000)

View File

@@ -1,8 +1,9 @@
from fastmcp import FastMCP
from mcps.test.test import weather_mcp
async def create_mcp_app():
main_mcp = FastMCP("MCP 主服务")
await main_mcp.import_server(weather_mcp , prefix="test")
return main_mcp.http_app()
await main_mcp.import_server(weather_mcp, prefix="test")
return main_mcp.http_app()

View File

@@ -1,3 +1,3 @@
from fastmcp import FastMCP
weather_mcp = FastMCP(name="WeatherService")
weather_mcp = FastMCP(name="WeatherService")

View File

@@ -1,6 +1,7 @@
from sqlmodel import Session, SQLModel, create_engine
from config import Settings
from config import Settings
from model.model import Department, Employee
PGSQL = Settings().PGSQL
@@ -18,3 +19,8 @@ def get_engine():
def get_session():
return Session(get_engine())
__all__ = [
"Department",
"Employee",
]

View File

@@ -1,22 +1,92 @@
from sqlmodel import SQLModel, Field, Column, JSON
from datetime import datetime
from typing import Optional
from sqlalchemy import Column, DateTime, JSON, func
from sqlmodel import Field, SQLModel
class Department(SQLModel, table = True):
did: int = Field(default=None, primary_key=True)
dname: str = Field(max_length=100)
name_en: str = Field(max_length=100)
department_leader: list[int] = Field(default=[], sa_column=Column(JSON))
parent_id: int = Field(default=0)
class Department(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
wecom_dept_id: str = Field(default=None, index=True)
dname: str = Field(
max_length=100,
description="部门名称",
)
name_en: str = Field(
max_length=100,
description="部门英文名",
)
department_leader: list[str] = Field(
default_factory=list,
sa_column=Column(JSON),
description="部门负责人 user_id 列表",
)
parent_id: str = Field(default=0, index=True)
order: int = Field(default=0)
class Employee(SQLModel, table = True):
userid: int = Field(default=None, primary_key=True)
ename: str = Field(max_length=100)
dept_id: int = Field(foreign_key='Department.did')
open_userid: str = Field(max_length=100)
created_at: datetime = Field(
default_factory=datetime.now,
sa_column=Column(
DateTime(timezone=True),
server_default=func.now(),
nullable=False,
),
)
updated_at: datetime = Field(
default_factory=datetime.now,
sa_column=Column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
),
)
class Employee(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
wecom_user_id: str = Field(default=None, index=True)
ename: str = Field(
max_length=100,
description="员工姓名",
)
dept_ids: list[str] = Field(
default_factory=list,
sa_column=Column(JSON),
description="部门ID",
)
open_userid: str | None = Field(
default=None,
max_length=100,
index=True,
description="企业微信 user_id",
)
created_at: datetime = Field(
default_factory=datetime.now,
sa_column=Column(
DateTime(timezone=True),
server_default=func.now(),
nullable=False,
),
)
updated_at: datetime = Field(
default_factory=datetime.now,
sa_column=Column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
),
)

View File

@@ -25,4 +25,7 @@ dependencies = [
]
[dependency-groups]
dev = []
dev = [
"isort>=7.0.0",
"ruff>=0.14.11",
]

View File

@@ -1,15 +1,14 @@
import json
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import PlainTextResponse
from uvicorn.server import logger
from utils.wxcom import wxcpt
from utils.wxcom import (
decrypt_message,
get_request_params
)
from utils.wxcom import decrypt_message, get_request_params, wxcpt
router = APIRouter()
@router.get("/callback")
async def verify_url(msg_signature: str, timestamp: str, nonce: str, echostr: str):
"""验证URL有效性"""
@@ -29,20 +28,21 @@ async def verify_url(msg_signature: str, timestamp: str, nonce: str, echostr: st
logger.error(f"验证过程发生错误: {str(e)}")
raise HTTPException(status_code=500, detail="服务器内部错误")
@router.post("/callback")
async def receive_message(request: Request):
"""接收并处理企业微信消息"""
try:
# 获取请求参数并验证,返回请求体、消息签名、时间戳和随机数
body, msg_signature, timestamp, nonce = await get_request_params(request)
# 对请求体进行解密,得到解密后的消息字典
xml_dict : dict= decrypt_message(body, msg_signature, timestamp, nonce)
logger.info(f"解密后的消息字典: \n {json.dumps(xml_dict.get("xml") , ensure_ascii=False , indent=2)}")
xml_dict: dict = decrypt_message(body, msg_signature, timestamp, nonce)
logger.info(
f"解密后的消息字典: \n {json.dumps(xml_dict.get('xml'), ensure_ascii=False, indent=2)}"
)
# 处理消息
# subscription(xml_dict)
except Exception as e:
logger.error(f"处理消息时发生错误: {str(e)}")
raise HTTPException(status_code=500, detail="服务器内部错误")
raise HTTPException(status_code=500, detail="服务器内部错误")

View File

@@ -1,7 +1,9 @@
from fastapi import FastAPI
from fastscheduler.fastapi_integration import create_scheduler_routes
from scheduler.scheduler import scheduler
def init_scheduler_router(app : FastAPI):
def init_scheduler_router(app: FastAPI):
app.include_router(create_scheduler_routes(scheduler))
scheduler.start()
scheduler.start()

View File

@@ -1,9 +1,11 @@
from fastscheduler import FastScheduler
from service.sync.department import sync_department
from service.sync.employee import sync_department_user
scheduler = FastScheduler(quiet=True)
@scheduler.every(10).seconds
def background_task():
print("Background work")
@scheduler.daily.at("04:00")
async def background_task():
await sync_department()
await sync_department_user()

View File

@@ -1,15 +1,15 @@
from service.wecom import Wecom
from config import Settings
from service.wecom import Wecom
from utils.sing import SingletonProvider
# 获取单例函数
def get_wecom_single() -> Wecom:
wecom = Wecom(
Settings().WECOM_CORPID,Settings().WECOM_CORPSECRET
)
wecom = Wecom(Settings().WECOM_CORPID, Settings().WECOM_CORPSECRET)
WECOM_PROXY = Settings().WECOM_PROXY
if WECOM_PROXY and WECOM_PROXY != "":
wecom.BASE_URL = WECOM_PROXY
wecom.BASE_URL = WECOM_PROXY + wecom.BASE_URL
return wecom
get_wecom = SingletonProvider(get_wecom_single)
get_wecom = SingletonProvider(get_wecom_single)

0
service/sync/__init__.py Normal file
View File

View File

@@ -0,0 +1,38 @@
from service import get_wecom
from model import get_session, Department
from sqlmodel import delete, select
async def sync_department():
wecom = get_wecom()
department_res = await wecom.get_departments()
with get_session() as session:
# 删除原来的数据
stmt = delete(Department)
session.execute(stmt)
session.commit()
# DepartmentInfoItem(id=302, name='亮剑一部一组-梁鹏涛', name_en=None, department_leader=['LiangPengTao'], parentid=96, order=100000000)
# 插入新的数据
for index, item in enumerate(department_res):
new_dept = Department(
id=index + 1,
wecom_dept_id=str(item.id),
dname=str(item.name),
name_en=str(item.name_en),
department_leader=item.department_leader or [],
parent_id=str(item.parentid),
order=item.order or 0,
)
session.add(new_dept)
session.commit()
def check_department_datebase():
with get_session() as session:
has = session.exec(select(Department)).first()
if not has:
return False
return True

37
service/sync/employee.py Normal file
View File

@@ -0,0 +1,37 @@
from service import get_wecom
from model import get_session, Employee
from sqlmodel import delete, select
async def sync_department_user():
wecom = get_wecom()
dept_res = await wecom.get_departments()
with get_session() as session:
# 删除原来的数据
stmt = delete(Employee)
session.execute(stmt)
session.commit()
index_id = 1
for dept in dept_res:
user_res = await wecom.get_user_in_department(dept.id)
for item in user_res:
new_employee = Employee(
id=index_id,
wecom_user_id=item.userid,
ename=item.name,
dept_ids=[str(i) for i in item.department],
open_userid=item.open_userid,
)
index_id += 1
session.add(new_employee)
session.commit()
def check_employee_datebase():
with get_session() as session:
has = session.exec(select(Employee)).first()
if not has:
return False
return True

View File

@@ -1,11 +1,8 @@
from datetime import datetime, timedelta
from wecom.exceptions.general import SDKException
from wecom.schemas.token import (
AccessTokenInfo,
AccessTokenParams,
)
from wecom.utils.requests import HttpxRequest
from service.wecom.exceptions.general import SDKException
from service.wecom.schemas.token import AccessTokenInfo, AccessTokenParams
from service.wecom.utils.requests import HttpxRequest
BASE_URL: str = "https://qyapi.weixin.qq.com/cgi-bin"

View File

@@ -1,17 +1,17 @@
from wecom.exceptions.general import SDKException
from wecom.modules.base import WecomBaseClient
from wecom.schemas.departments import (
from service.wecom.exceptions.general import SDKException
from service.wecom.modules.base import WecomBaseClient
from service.wecom.schemas.departments import (
CreateDepartmentInfo,
CreateDepartmentParams,
DepartmentInfo,
DepartmentInfoItem,
UpdateDepartmentInfo,
UpdateDepartmentParams,
)
from wecom.utils.requests import HttpxRequest
from service.wecom.utils.requests import HttpxRequest
class WecomDepartmentClient(WecomBaseClient):
async def create_departments(self, data: CreateDepartmentParams) -> int:
"""
创建部门
@@ -64,7 +64,7 @@ class WecomDepartmentClient(WecomBaseClient):
else:
raise SDKException(resp.errcode, resp.errmsg)
async def get_departments(self, id: int = None) -> list[DepartmentInfo]:
async def get_departments(self, id: int = None) -> list[DepartmentInfoItem]:
"""
获取部门列表
@param id: 部门id。获取指定部门及其下的子部门。

View File

@@ -1,18 +1,17 @@
from typing import Literal
from wecom.exceptions.general import SDKException
from wecom.modules.base import WecomBaseClient
from wecom.schemas.message import (
from service.wecom.exceptions.general import SDKException
from service.wecom.modules.base import WecomBaseClient
from service.wecom.schemas.message import (
MessageParams,
RecallMessageInfo,
RecallMessageParams,
SendMessageInfo,
)
from wecom.utils.requests import HttpxRequest
from service.wecom.utils.requests import HttpxRequest
class WecomMessageClient(WecomBaseClient):
async def send_message(
self,
data: MessageParams,

View File

@@ -1,7 +1,7 @@
from wecom.modules.base import WecomBaseClient
from wecom.modules.department import WecomDepartmentClient
from wecom.modules.message import WecomMessageClient
from wecom.modules.users import WecomUsersClient
from service.wecom.modules.base import WecomBaseClient
from service.wecom.modules.department import WecomDepartmentClient
from service.wecom.modules.message import WecomMessageClient
from service.wecom.modules.users import WecomUsersClient
class Wecom(

View File

@@ -1,12 +1,13 @@
from wecom.exceptions.general import SDKException
from wecom.modules.base import WecomBaseClient
from wecom.schemas.departments import DepartmentInfo
from wecom.schemas.users import (
from service.wecom.exceptions.general import SDKException
from service.wecom.modules.base import WecomBaseClient
from service.wecom.schemas.departments import DepartmentInfo
from service.wecom.schemas.users import (
DepartmentUserDetailInfo,
DepartmentUserInfo,
UserInfo,
UserSimpleInfo,
)
from wecom.utils.requests import HttpxRequest
from service.wecom.utils.requests import HttpxRequest
class WecomUsersClient(WecomBaseClient):
@@ -49,7 +50,7 @@ class WecomUsersClient(WecomBaseClient):
else:
raise SDKException(resp.errcode, resp.errmsg)
async def get_user_in_department(self, department_id: int) -> dict:
async def get_user_in_department(self, department_id: int) -> list[UserSimpleInfo]:
"""
读取部门成员简要信息
@param department_id: 获取的部门id
@@ -65,7 +66,7 @@ class WecomUsersClient(WecomBaseClient):
resp = DepartmentUserInfo(**await HttpxRequest.get(url=url, params=params))
if resp.errcode == 0:
return resp.model_dump(exclude={"errcode", "errmsg"})
return resp.userlist
else:
raise SDKException(resp.errcode, resp.errmsg)

View File

@@ -1,6 +1,6 @@
from typing import AnyStr, List
from typing import List
from wecom.schemas.base import BaseSchema
from service.wecom.schemas.base import BaseSchema
class CreateDepartmentParams(BaseSchema):
@@ -26,23 +26,23 @@ class UpdateDepartmentParams(CreateDepartmentParams): ...
class UpdateDepartmentInfo(BaseSchema):
errcode: int
errmsg: AnyStr
errmsg: str
class CreateDepartmentInfo(BaseSchema):
errcode: int
errmsg: AnyStr
errmsg: str
id: int
class DepartmentInfo(BaseSchema):
class DepartmentInfoItem(BaseSchema):
"""
部门单体响应数据
"""
id: int
name: AnyStr
name_en: AnyStr | None = None
name: str
name_en: str | None = None
department_leader: List[str] | None = None
parentid: int | None = None
order: int | None = None
@@ -54,5 +54,5 @@ class DepartmentInfo(BaseSchema):
"""
errcode: int
errmsg: AnyStr
department: List[DepartmentInfo]
errmsg: str
department: List[DepartmentInfoItem]

View File

@@ -1,6 +1,6 @@
from typing import AnyStr, Literal
from typing import Literal
from wecom.schemas.base import BaseSchema
from service.wecom.schemas.base import BaseSchema
class MessageParams(BaseSchema):
@@ -24,9 +24,9 @@ class MessageParams(BaseSchema):
touser、toparty、totag不能同时为空后面不再强调
"""
touser: AnyStr | None = None
toparty: AnyStr | None = None
totag: AnyStr | None = None
touser: str | None = None
toparty: str | None = None
totag: str | None = None
msgtype: Literal[
"text", "image", "voice", "video", "textcard", "news", "mpnews", "markdown"
]
@@ -55,13 +55,13 @@ class SendMessageInfo(BaseSchema):
"""
errcode: int
errmsg: AnyStr
invaliduser: AnyStr | None = None
invalidparty: AnyStr | None = None
invalidtag: AnyStr | None = None
unlicenseduser: AnyStr | None = None
msgid: AnyStr | None = None
response_code: AnyStr | None = None
errmsg: str
invaliduser: str | None = None
invalidparty: str | None = None
invalidtag: str | None = None
unlicenseduser: str | None = None
msgid: str | None = None
response_code: str | None = None
class SendMessageInvalid(BaseSchema):
@@ -70,11 +70,11 @@ class SendMessageInvalid(BaseSchema):
"""
errmsg: AnyStr
invaliduser: AnyStr | None = None
invalidparty: AnyStr | None = None
invalidtag: AnyStr | None = None
unlicenseduser: AnyStr | None = None
errmsg: str
invaliduser: str | None = None
invalidparty: str | None = None
invalidtag: str | None = None
unlicenseduser: str | None = None
class RecallMessageParams(BaseSchema):
@@ -82,7 +82,7 @@ class RecallMessageParams(BaseSchema):
撤回消息请求参数
"""
msgid: AnyStr
msgid: str
class RecallMessageInfo(BaseSchema):
@@ -91,4 +91,4 @@ class RecallMessageInfo(BaseSchema):
"""
errcode: int
errmsg: AnyStr
errmsg: str

View File

@@ -1,4 +1,4 @@
from wecom.schemas.base import BaseSchema
from service.wecom.schemas.base import BaseSchema
class AccessTokenParams(BaseSchema):

View File

@@ -1,6 +1,6 @@
from typing import AnyStr, List
from typing import List
from wecom.schemas.base import BaseSchema
from service.wecom.schemas.base import BaseSchema
class UserInfo(BaseSchema):
@@ -9,31 +9,31 @@ class UserInfo(BaseSchema):
"""
errcode: int
errmsg: AnyStr
userid: AnyStr | None = None
name: AnyStr | None = None
errmsg: str
userid: str | None = None
name: str | None = None
department: List[int] | None = None
position: AnyStr | None = None
moblie: AnyStr | None = None
position: str | None = None
moblie: str | None = None
gender: int | None = None
email: AnyStr | None = None
email: str | None = None
status: int | None = None
class UserSimpleInfo(BaseSchema):
userid: AnyStr
name: AnyStr
userid: str
name: str
department: List[int]
open_userid: AnyStr | None = None
open_userid: str | None = None
class DepartmentUserInfo(BaseSchema):
errcode: int
errmsg: AnyStr
errmsg: str
userlist: List[UserSimpleInfo]
class DepartmentUserDetailInfo(BaseSchema):
errcode: int
errmsg: AnyStr
errmsg: str
userlist: List[UserInfo]

View File

@@ -1,9 +1,4 @@
from .wx_com import wxcpt
from .wx_utils import get_request_params,decrypt_message,extract_message_content
from .wx_utils import decrypt_message, extract_message_content, get_request_params
__all__ = [
"wxcpt",
"get_request_params",
"decrypt_message",
"extract_message_content"
]
__all__ = ["wxcpt", "get_request_params", "decrypt_message", "extract_message_content"]

View File

@@ -1,6 +1,7 @@
from uvicorn.server import logger
from config import Settings
from utils.wxcom.WXBizMsgCrypt3 import WXBizMsgCrypt
from utils.wxcom.WXBizMsgCrypt3 import WXBizMsgCrypt
def get_wxcpt():
@@ -15,20 +16,20 @@ def get_wxcpt():
required_configs = [
Settings().WECOM_APP_TOKEN,
Settings().WECOM_APP_ENCODING_AES_KEY,
Settings().WECOM_CORPID
Settings().WECOM_CORPID,
]
if not all(required_configs):
raise ValueError("企业微信配置不完整")
return WXBizMsgCrypt(
Settings().WECOM_APP_TOKEN, # 设置的Token
Settings().WECOM_APP_ENCODING_AES_KEY, # 设置密钥
Settings().WECOM_CORPID # 企业ID
Settings().WECOM_CORPID, # 企业ID
)
except Exception as e:
logger.error(f"初始化WXBizMsgCrypt失败: {str(e)}")
raise
wxcpt = get_wxcpt()

View File

@@ -4,6 +4,7 @@ from typing import Dict, Tuple, Union
import xmltodict
from fastapi import HTTPException, Request
from uvicorn.server import logger
from .wx_com import wxcpt
@@ -23,7 +24,10 @@ async def get_request_params(request: Request) -> Tuple[bytes, str, str, str]:
return body, msg_signature, timestamp, nonce
def decrypt_message(body: bytes, msg_signature: str, timestamp: str, nonce: str) -> dict:
def decrypt_message(
body: bytes, msg_signature: str, timestamp: str, nonce: str
) -> dict:
"""解密消息"""
ret, sMsg = wxcpt.DecryptMsg(body, msg_signature, timestamp, nonce)
if ret != 0:
@@ -35,6 +39,7 @@ def decrypt_message(body: bytes, msg_signature: str, timestamp: str, nonce: str)
return xml_dict
def extract_message_content(
xml_dict: Dict,
) -> Tuple[str, str, str, str, Union[Dict[str, Union[str, None]], str, None], str, str]:
@@ -82,7 +87,6 @@ def extract_message_content(
message_data = xml_content.get("Content")
logger.info(f"收到未知类型消息: {message_data}")
return {
"ToUserName": to_user_name,
"FromUserName": from_user_name,
@@ -91,4 +95,4 @@ def extract_message_content(
"MsgId": msg_id,
"AgentID": agent_id,
**message_data,
}
}

46
uv.lock generated
View File

@@ -651,6 +651,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
]
[[package]]
name = "isort"
version = "7.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/63/53/4f3c058e3bace40282876f9b553343376ee687f3c35a525dc79dbd450f88/isort-7.0.0.tar.gz", hash = "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187", size = 805049, upload-time = "2025-10-11T13:30:59.107Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/ed/e3705d6d02b4f7aea715a353c8ce193efd0b5db13e204df895d38734c244/isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1", size = 94672, upload-time = "2025-10-11T13:30:57.665Z" },
]
[[package]]
name = "jaraco-classes"
version = "3.4.0"
@@ -1659,6 +1668,32 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" },
]
[[package]]
name = "ruff"
version = "0.14.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d4/77/9a7fe084d268f8855d493e5031ea03fa0af8cc05887f638bf1c4e3363eb8/ruff-0.14.11.tar.gz", hash = "sha256:f6dc463bfa5c07a59b1ff2c3b9767373e541346ea105503b4c0369c520a66958", size = 5993417, upload-time = "2026-01-08T19:11:58.322Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f0/a6/a4c40a5aaa7e331f245d2dc1ac8ece306681f52b636b40ef87c88b9f7afd/ruff-0.14.11-py3-none-linux_armv6l.whl", hash = "sha256:f6ff2d95cbd335841a7217bdfd9c1d2e44eac2c584197ab1385579d55ff8830e", size = 12951208, upload-time = "2026-01-08T19:12:09.218Z" },
{ url = "https://files.pythonhosted.org/packages/5c/5c/360a35cb7204b328b685d3129c08aca24765ff92b5a7efedbdd6c150d555/ruff-0.14.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f6eb5c1c8033680f4172ea9c8d3706c156223010b8b97b05e82c59bdc774ee6", size = 13330075, upload-time = "2026-01-08T19:12:02.549Z" },
{ url = "https://files.pythonhosted.org/packages/1b/9e/0cc2f1be7a7d33cae541824cf3f95b4ff40d03557b575912b5b70273c9ec/ruff-0.14.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2fc34cc896f90080fca01259f96c566f74069a04b25b6205d55379d12a6855e", size = 12257809, upload-time = "2026-01-08T19:12:00.366Z" },
{ url = "https://files.pythonhosted.org/packages/a7/e5/5faab97c15bb75228d9f74637e775d26ac703cc2b4898564c01ab3637c02/ruff-0.14.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53386375001773ae812b43205d6064dae49ff0968774e6befe16a994fc233caa", size = 12678447, upload-time = "2026-01-08T19:12:13.899Z" },
{ url = "https://files.pythonhosted.org/packages/1b/33/e9767f60a2bef779fb5855cab0af76c488e0ce90f7bb7b8a45c8a2ba4178/ruff-0.14.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a697737dce1ca97a0a55b5ff0434ee7205943d4874d638fe3ae66166ff46edbe", size = 12758560, upload-time = "2026-01-08T19:11:42.55Z" },
{ url = "https://files.pythonhosted.org/packages/eb/84/4c6cf627a21462bb5102f7be2a320b084228ff26e105510cd2255ea868e5/ruff-0.14.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6845ca1da8ab81ab1dce755a32ad13f1db72e7fba27c486d5d90d65e04d17b8f", size = 13599296, upload-time = "2026-01-08T19:11:30.371Z" },
{ url = "https://files.pythonhosted.org/packages/88/e1/92b5ed7ea66d849f6157e695dc23d5d6d982bd6aa8d077895652c38a7cae/ruff-0.14.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e36ce2fd31b54065ec6f76cb08d60159e1b32bdf08507862e32f47e6dde8bcbf", size = 15048981, upload-time = "2026-01-08T19:12:04.742Z" },
{ url = "https://files.pythonhosted.org/packages/61/df/c1bd30992615ac17c2fb64b8a7376ca22c04a70555b5d05b8f717163cf9f/ruff-0.14.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:590bcc0e2097ecf74e62a5c10a6b71f008ad82eb97b0a0079e85defe19fe74d9", size = 14633183, upload-time = "2026-01-08T19:11:40.069Z" },
{ url = "https://files.pythonhosted.org/packages/04/e9/fe552902f25013dd28a5428a42347d9ad20c4b534834a325a28305747d64/ruff-0.14.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53fe71125fc158210d57fe4da26e622c9c294022988d08d9347ec1cf782adafe", size = 14050453, upload-time = "2026-01-08T19:11:37.555Z" },
{ url = "https://files.pythonhosted.org/packages/ae/93/f36d89fa021543187f98991609ce6e47e24f35f008dfe1af01379d248a41/ruff-0.14.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a35c9da08562f1598ded8470fcfef2afb5cf881996e6c0a502ceb61f4bc9c8a3", size = 13757889, upload-time = "2026-01-08T19:12:07.094Z" },
{ url = "https://files.pythonhosted.org/packages/b7/9f/c7fb6ecf554f28709a6a1f2a7f74750d400979e8cd47ed29feeaa1bd4db8/ruff-0.14.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0f3727189a52179393ecf92ec7057c2210203e6af2676f08d92140d3e1ee72c1", size = 13955832, upload-time = "2026-01-08T19:11:55.064Z" },
{ url = "https://files.pythonhosted.org/packages/db/a0/153315310f250f76900a98278cf878c64dfb6d044e184491dd3289796734/ruff-0.14.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:eb09f849bd37147a789b85995ff734a6c4a095bed5fd1608c4f56afc3634cde2", size = 12586522, upload-time = "2026-01-08T19:11:35.356Z" },
{ url = "https://files.pythonhosted.org/packages/2f/2b/a73a2b6e6d2df1d74bf2b78098be1572191e54bec0e59e29382d13c3adc5/ruff-0.14.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c61782543c1231bf71041461c1f28c64b961d457d0f238ac388e2ab173d7ecb7", size = 12724637, upload-time = "2026-01-08T19:11:47.796Z" },
{ url = "https://files.pythonhosted.org/packages/f0/41/09100590320394401cd3c48fc718a8ba71c7ddb1ffd07e0ad6576b3a3df2/ruff-0.14.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82ff352ea68fb6766140381748e1f67f83c39860b6446966cff48a315c3e2491", size = 13145837, upload-time = "2026-01-08T19:11:32.87Z" },
{ url = "https://files.pythonhosted.org/packages/3b/d8/e035db859d1d3edf909381eb8ff3e89a672d6572e9454093538fe6f164b0/ruff-0.14.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:728e56879df4ca5b62a9dde2dd0eb0edda2a55160c0ea28c4025f18c03f86984", size = 13850469, upload-time = "2026-01-08T19:12:11.694Z" },
{ url = "https://files.pythonhosted.org/packages/4e/02/bb3ff8b6e6d02ce9e3740f4c17dfbbfb55f34c789c139e9cd91985f356c7/ruff-0.14.11-py3-none-win32.whl", hash = "sha256:337c5dd11f16ee52ae217757d9b82a26400be7efac883e9e852646f1557ed841", size = 12851094, upload-time = "2026-01-08T19:11:45.163Z" },
{ url = "https://files.pythonhosted.org/packages/58/f1/90ddc533918d3a2ad628bc3044cdfc094949e6d4b929220c3f0eb8a1c998/ruff-0.14.11-py3-none-win_amd64.whl", hash = "sha256:f981cea63d08456b2c070e64b79cb62f951aa1305282974d4d5216e6e0178ae6", size = 14001379, upload-time = "2026-01-08T19:11:52.591Z" },
{ url = "https://files.pythonhosted.org/packages/c4/1c/1dbe51782c0e1e9cfce1d1004752672d2d4629ea46945d19d731ad772b3b/ruff-0.14.11-py3-none-win_arm64.whl", hash = "sha256:649fb6c9edd7f751db276ef42df1f3df41c38d67d199570ae2a7bd6cbc3590f0", size = 12938644, upload-time = "2026-01-08T19:11:50.027Z" },
]
[[package]]
name = "secretstorage"
version = "3.5.0"
@@ -1981,6 +2016,12 @@ dependencies = [
{ name = "xmltodict" },
]
[package.dev-dependencies]
dev = [
{ name = "isort" },
{ name = "ruff" },
]
[package.metadata]
requires-dist = [
{ name = "apscheduler", specifier = ">=3.11.0" },
@@ -2003,7 +2044,10 @@ requires-dist = [
]
[package.metadata.requires-dev]
dev = []
dev = [
{ name = "isort", specifier = ">=7.0.0" },
{ name = "ruff", specifier = ">=0.14.11" },
]
[[package]]
name = "wrapt"