This commit is contained in:
2026-01-15 18:08:08 +08:00
parent 5586e98e51
commit 4a51ec89cc
21 changed files with 184 additions and 72 deletions

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from uvicorn.server import logger from uvicorn.server import logger
def init_database(): def init_database():
from model import create_db_and_tables from model import create_db_and_tables
@@ -10,8 +11,10 @@ def init_database():
create_db_and_tables() create_db_and_tables()
logger.info("[数据库] 数据库初始化完成 ✅") logger.info("[数据库] 数据库初始化完成 ✅")
def init_scheduler(app : FastAPI):
def init_scheduler(app: FastAPI):
from scheduler import init_scheduler_router from scheduler import init_scheduler_router
logger.info("[定时任务] 初始化定时任务 📦") logger.info("[定时任务] 初始化定时任务 📦")
init_scheduler_router(app) init_scheduler_router(app)
logger.info("[定时任务] 定时任务初始化完成 ✅") logger.info("[定时任务] 定时任务初始化完成 ✅")
@@ -29,12 +32,15 @@ def import_router(app: FastAPI):
app.include_router(router) app.include_router(router)
logger.info(f"[导入路由] 路由导入完成 ✅") logger.info(f"[导入路由] 路由导入完成 ✅")
async def import_mcp_server(app: FastAPI): async def import_mcp_server(app: FastAPI):
logger.info(f"[导入MCP] 开始导入MCP 🛣️") logger.info(f"[导入MCP] 开始导入MCP 🛣️")
from mcps import create_mcp_app 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导入完成 ✅") logger.info(f"[导入MCP] MCP导入完成 ✅")
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
logger.info(f"[生命周期] 应用启动 🚀") logger.info(f"[生命周期] 应用启动 🚀")

View File

@@ -4,7 +4,6 @@ from handler.exception import install as exception_install
from lifespan import lifespan from lifespan import lifespan
from mcps import create_mcp_app from mcps import create_mcp_app
app = FastAPI(lifespan=lifespan) app = FastAPI(lifespan=lifespan)
exception_install(app) exception_install(app)

View File

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

View File

@@ -1,6 +1,7 @@
from sqlmodel import Session, SQLModel, create_engine from sqlmodel import Session, SQLModel, create_engine
from config import Settings
from config import Settings
from model.model import Department, Employee, Tenant
PGSQL = Settings().PGSQL PGSQL = Settings().PGSQL
@@ -17,4 +18,3 @@ def get_engine():
def get_session(): def get_session():
return Session(get_engine()) return Session(get_engine())

View File

@@ -1,22 +1,85 @@
from sqlmodel import SQLModel, Field, Column, JSON from datetime import datetime
from sqlalchemy import JSON, Column, DateTime, func
from sqlmodel import JSON, Field, SQLModel
class Department(SQLModel, table = True): class TenantTimeMixin(SQLModel):
did: int = Field(default=None, primary_key=True) tenant_id: int = Field(index=True, description="租户ID")
created_at: datetime = Field(
sa_column=Column(
DateTime(timezone=True),
server_default=func.now(),
nullable=False,
)
)
updated_at: datetime = Field(
sa_column=Column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
)
)
class Department(TenantTimeMixin, SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
dname: str = Field(max_length=100) dname: str = Field(max_length=100)
name_en: 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) department_leader: list[int] = Field(default_factory=list, sa_column=Column(JSON))
parent_id: int = Field(default=0, index=True)
order: int = Field(default=0) order: int = Field(default=0)
class Employee(SQLModel, table = True):
userid: int = Field(default=None, primary_key=True) class Employee(TenantTimeMixin, SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
ename: str = Field(max_length=100) ename: str = Field(max_length=100)
dept_id: int = Field(foreign_key='Department.did')
open_userid: str = Field(max_length=100) dept_id: int = Field(foreign_key="department.id", index=True)
open_userid: str = Field(max_length=100, index=True)
class Tenant(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
# ========== 基础信息 ==========
name: str = Field(max_length=100, index=True, description="租户名称 / 企业名称")
# ========== 企业微信配置 ==========
wecom_corp_id: str = Field(max_length=64, index=True, description="企业微信 CorpID")
wecom_corp_secret: str = Field(max_length=128, description="企业微信应用 Secret")
wecom_agent_id: int = Field(description="企业微信应用 AgentId")
wecom_token: str = Field(max_length=64, description="企业微信回调 Token")
wecom_encoding_aes_key: str = Field(
max_length=64, description="企业微信回调 EncodingAESKey"
)
# ========== 时间字段 ==========
created_at: datetime = Field(
sa_column=Column(
DateTime(timezone=True),
server_default=func.now(),
nullable=False,
)
)
updated_at: datetime = Field(
sa_column=Column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
)
)

View File

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

View File

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

View File

@@ -1,9 +1,12 @@
from fastscheduler import FastScheduler from fastscheduler import FastScheduler
from service import get_wecom
scheduler = FastScheduler(quiet=True) scheduler = FastScheduler(quiet=True)
@scheduler.every(10).seconds @scheduler.every(4).hours
def background_task(): async def background_task():
print("Background work") wecom = get_wecom()
await wecom.get_departments()

View File

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

View File

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

View File

@@ -11,7 +11,6 @@ from wecom.utils.requests import HttpxRequest
class WecomDepartmentClient(WecomBaseClient): class WecomDepartmentClient(WecomBaseClient):
async def create_departments(self, data: CreateDepartmentParams) -> int: async def create_departments(self, data: CreateDepartmentParams) -> int:
""" """
创建部门 创建部门

View File

@@ -12,7 +12,6 @@ from wecom.utils.requests import HttpxRequest
class WecomMessageClient(WecomBaseClient): class WecomMessageClient(WecomBaseClient):
async def send_message( async def send_message(
self, self,
data: MessageParams, data: MessageParams,

View File

@@ -1,11 +1,7 @@
from wecom.exceptions.general import SDKException from wecom.exceptions.general import SDKException
from wecom.modules.base import WecomBaseClient from wecom.modules.base import WecomBaseClient
from wecom.schemas.departments import DepartmentInfo from wecom.schemas.departments import DepartmentInfo
from wecom.schemas.users import ( from wecom.schemas.users import DepartmentUserDetailInfo, DepartmentUserInfo, UserInfo
DepartmentUserDetailInfo,
DepartmentUserInfo,
UserInfo,
)
from wecom.utils.requests import HttpxRequest from wecom.utils.requests import HttpxRequest

View File

@@ -1,9 +1,4 @@
from .wx_com import wxcpt 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__ = [ __all__ = ["wxcpt", "get_request_params", "decrypt_message", "extract_message_content"]
"wxcpt",
"get_request_params",
"decrypt_message",
"extract_message_content"
]

View File

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

View File

@@ -4,6 +4,7 @@ from typing import Dict, Tuple, Union
import xmltodict import xmltodict
from fastapi import HTTPException, Request from fastapi import HTTPException, Request
from uvicorn.server import logger from uvicorn.server import logger
from .wx_com import wxcpt 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 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) ret, sMsg = wxcpt.DecryptMsg(body, msg_signature, timestamp, nonce)
if ret != 0: if ret != 0:
@@ -35,6 +39,7 @@ def decrypt_message(body: bytes, msg_signature: str, timestamp: str, nonce: str)
return xml_dict return xml_dict
def extract_message_content( def extract_message_content(
xml_dict: Dict, xml_dict: Dict,
) -> Tuple[str, str, str, str, Union[Dict[str, Union[str, None]], str, None], str, str]: ) -> 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") message_data = xml_content.get("Content")
logger.info(f"收到未知类型消息: {message_data}") logger.info(f"收到未知类型消息: {message_data}")
return { return {
"ToUserName": to_user_name, "ToUserName": to_user_name,
"FromUserName": from_user_name, "FromUserName": from_user_name,

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" }, { 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]] [[package]]
name = "jaraco-classes" name = "jaraco-classes"
version = "3.4.0" 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" }, { 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]] [[package]]
name = "secretstorage" name = "secretstorage"
version = "3.5.0" version = "3.5.0"
@@ -1981,6 +2016,12 @@ dependencies = [
{ name = "xmltodict" }, { name = "xmltodict" },
] ]
[package.dev-dependencies]
dev = [
{ name = "isort" },
{ name = "ruff" },
]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "apscheduler", specifier = ">=3.11.0" }, { name = "apscheduler", specifier = ">=3.11.0" },
@@ -2003,7 +2044,10 @@ requires-dist = [
] ]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [] dev = [
{ name = "isort", specifier = ">=7.0.0" },
{ name = "ruff", specifier = ">=0.14.11" },
]
[[package]] [[package]]
name = "wrapt" name = "wrapt"