Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 39b6180fb8 | |||
| 4e08b9a986 | |||
| 1f5140c0fe | |||
| 1496af0973 | |||
| 2b37953cf5 | |||
| e77a380d45 | |||
| 723c7817b6 | |||
| 4a51ec89cc | |||
| 5586e98e51 |
@@ -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 = ""
|
||||
|
||||
@@ -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]
|
||||
|
||||
149
lifespan.py
149
lifespan.py
@@ -1,8 +1,114 @@
|
||||
import asyncio
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from uvicorn.server import logger
|
||||
|
||||
|
||||
class ChainBuilder:
|
||||
"""支持链式调用的建造者类,支持同步和异步方法"""
|
||||
|
||||
def __init__(self):
|
||||
self._tasks = []
|
||||
|
||||
def add(self, func, *args, **kwargs):
|
||||
"""添加同步或异步任务到链中"""
|
||||
self._tasks.append((func, args, kwargs))
|
||||
return self
|
||||
|
||||
def adds(self, *funcs_or_tuples):
|
||||
"""添加一个或多个同步或异步任务到链中
|
||||
|
||||
支持多种调用方式:
|
||||
1. 单个函数: add(func, *args, **kwargs)
|
||||
2. 多个函数: add((func1, args1, kwargs1), (func2, args2, kwargs2), ...)
|
||||
3. 混合方式: add(func1, *args1, **kwargs1), (func2, args2, kwargs2), ...
|
||||
"""
|
||||
for item in funcs_or_tuples:
|
||||
if isinstance(item, tuple) and len(item) == 3:
|
||||
# 如果是三元组 (func, args, kwargs)
|
||||
func, args, kwargs = item
|
||||
self._tasks.append((func, args, kwargs))
|
||||
elif callable(item):
|
||||
# 如果是单个函数,需要检查后续参数
|
||||
if (
|
||||
len(funcs_or_tuples) >= 3
|
||||
and isinstance(funcs_or_tuples[1], tuple)
|
||||
and isinstance(funcs_or_tuples[2], dict)
|
||||
):
|
||||
# 如果是 add(func, args, kwargs) 格式
|
||||
func = item
|
||||
args = funcs_or_tuples[1] if len(funcs_or_tuples) > 1 else ()
|
||||
kwargs = funcs_or_tuples[2] if len(funcs_or_tuples) > 2 else {}
|
||||
self._tasks.append((func, args, kwargs))
|
||||
break # 处理完这个函数后退出循环
|
||||
else:
|
||||
# 单个函数没有参数
|
||||
self._tasks.append((item, (), {}))
|
||||
else:
|
||||
raise ValueError(f"不支持的参数类型: {type(item)}")
|
||||
return self
|
||||
|
||||
async def _execute_async(self):
|
||||
"""异步执行所有任务"""
|
||||
for func, args, kwargs in self._tasks:
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
await func(*args, **kwargs)
|
||||
else:
|
||||
# 如果是同步函数,在事件循环中运行
|
||||
func(*args, **kwargs)
|
||||
|
||||
def __call__(self):
|
||||
"""同步调用接口"""
|
||||
import asyncio
|
||||
|
||||
# 检查是否有异步任务
|
||||
has_async = any(asyncio.iscoroutinefunction(func) for func, _, _ in self._tasks)
|
||||
|
||||
if has_async:
|
||||
# 如果有异步任务,创建并运行事件循环
|
||||
loop = asyncio.get_event_loop()
|
||||
if loop.is_running():
|
||||
# 如果事件循环已经在运行,创建任务
|
||||
asyncio.create_task(self._execute_async())
|
||||
else:
|
||||
# 否则运行事件循环
|
||||
loop.run_until_complete(self._execute_async())
|
||||
else:
|
||||
# 如果都是同步任务,直接执行
|
||||
for func, args, kwargs in self._tasks:
|
||||
func(*args, **kwargs)
|
||||
|
||||
return self
|
||||
|
||||
async def __aenter__(self):
|
||||
"""异步上下文管理器入口"""
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
"""异步上下文管理器出口"""
|
||||
pass
|
||||
|
||||
async def execute_async(self):
|
||||
"""显式异步执行方法"""
|
||||
await self._execute_async()
|
||||
return self
|
||||
|
||||
|
||||
async def data_base_init():
|
||||
from service.sync.department import check_department_datebase, sync_department
|
||||
from service.sync.employee import check_employee_datebase, sync_department_user
|
||||
|
||||
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,38 +116,51 @@ 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"[激活配置] 加载配置 ⚙️")
|
||||
logger.info("[激活配置] 加载配置 ⚙️")
|
||||
from config import Settings # noqa
|
||||
|
||||
|
||||
def import_router(app: FastAPI):
|
||||
logger.info(f"[导入路由] 开始导入路由 🛣️")
|
||||
logger.info("[导入路由] 开始导入路由 🛣️")
|
||||
from router import router
|
||||
|
||||
app.include_router(router)
|
||||
logger.info(f"[导入路由] 路由导入完成 ✅")
|
||||
logger.info("[导入路由] 路由导入完成 ✅")
|
||||
|
||||
|
||||
async def import_mcp_server(app: FastAPI):
|
||||
logger.info(f"[导入MCP] 开始导入MCP 🛣️")
|
||||
logger.info("[导入MCP] 开始导入MCP 🛣️")
|
||||
from mcps import create_mcp_app
|
||||
app.mount("/app" , await create_mcp_app())
|
||||
logger.info(f"[导入MCP] MCP导入完成 ✅")
|
||||
|
||||
app.mount("/app", await create_mcp_app())
|
||||
logger.info("[导入MCP] MCP导入完成 ✅")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
logger.info(f"[生命周期] 应用启动 🚀")
|
||||
active_config()
|
||||
init_database()
|
||||
import_router(app)
|
||||
init_scheduler(app)
|
||||
await import_mcp_server(app)
|
||||
logger.info("[生命周期] 应用启动 🚀")
|
||||
builder = ChainBuilder()
|
||||
|
||||
# 激活配置
|
||||
builder.add(active_config)
|
||||
# 初始化数据库
|
||||
builder.add(init_database).add(data_base_init)
|
||||
# 导入MCP
|
||||
builder.add(import_mcp_server, app)
|
||||
# 导入路由
|
||||
builder.add(import_router, app)
|
||||
# 初始化定时任务
|
||||
builder.add(init_scheduler, app)
|
||||
await builder.execute_async()
|
||||
yield
|
||||
logger.info(f"[生命周期] 应用关闭 🔧✅")
|
||||
logger.info("[生命周期] 应用关闭 🔧✅")
|
||||
|
||||
3
main.py
3
main.py
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
from fastmcp import FastMCP
|
||||
|
||||
weather_mcp = FastMCP(name="WeatherService")
|
||||
weather_mcp = FastMCP(name="WeatherService")
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -25,4 +25,7 @@ dependencies = [
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = []
|
||||
dev = [
|
||||
"isort>=7.0.0",
|
||||
"ruff>=0.14.11",
|
||||
]
|
||||
|
||||
@@ -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="服务器内部错误")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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/callback/base.py
Normal file
0
service/callback/base.py
Normal file
0
service/sync/__init__.py
Normal file
0
service/sync/__init__.py
Normal file
38
service/sync/department.py
Normal file
38
service/sync/department.py
Normal 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
37
service/sync/employee.py
Normal 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
|
||||
@@ -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"
|
||||
|
||||
|
||||
28
service/wecom/modules/card.py
Normal file
28
service/wecom/modules/card.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from service.wecom.exceptions.general import SDKException
|
||||
from service.wecom.modules.base import WecomBaseClient
|
||||
from service.wecom.schemas.card import (
|
||||
GetCardRecord,
|
||||
GetCardRecordsRequest,
|
||||
GetCardRecordsResponse,
|
||||
)
|
||||
from service.wecom.utils.requests import HttpxRequest
|
||||
|
||||
|
||||
class WecomCardClient(WecomBaseClient):
|
||||
async def get_card_records(
|
||||
self, data: GetCardRecordsRequest
|
||||
) -> list[GetCardRecord]:
|
||||
"""
|
||||
获取打卡记录数据
|
||||
@param data: 获取打卡记录数据的参数
|
||||
"""
|
||||
url = self.BASE_URL + "/checkin/getcheckindata"
|
||||
params = {"access_token": await self.access_token}
|
||||
resp = GetCardRecordsResponse(
|
||||
**await HttpxRequest.post(url=url, params=params, json=data.model_dump())
|
||||
)
|
||||
|
||||
if resp.errcode == 0:
|
||||
return resp.checkindata
|
||||
else:
|
||||
raise SDKException(resp.errcode, resp.errmsg)
|
||||
@@ -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。获取指定部门及其下的子部门。
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
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.card import WecomCardClient
|
||||
from service.wecom.modules.department import WecomDepartmentClient
|
||||
from service.wecom.modules.message import WecomMessageClient
|
||||
from service.wecom.modules.users import WecomUsersClient
|
||||
|
||||
|
||||
class Wecom(
|
||||
WecomDepartmentClient, WecomUsersClient, WecomMessageClient, WecomBaseClient
|
||||
WecomDepartmentClient,
|
||||
WecomUsersClient,
|
||||
WecomMessageClient,
|
||||
WecomCardClient,
|
||||
WecomBaseClient,
|
||||
):
|
||||
pass
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
45
service/wecom/schemas/card.py
Normal file
45
service/wecom/schemas/card.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import datetime
|
||||
|
||||
from enum import Enum
|
||||
from service.wecom.schemas.base import BaseSchema
|
||||
|
||||
|
||||
class CheckinType(Enum):
|
||||
"""打卡类型"""
|
||||
|
||||
ON_OFF_DUTY = 1 # 上下班打卡
|
||||
OUTING = 2 # 外出打卡
|
||||
ALL = 3 # 全部打卡
|
||||
|
||||
|
||||
class GetCardRecordsRequest(BaseSchema):
|
||||
"""获取打卡记录请求"""
|
||||
|
||||
opencheckindatatype: CheckinType
|
||||
starttime: datetime.datetime
|
||||
endtime: datetime.datetime
|
||||
useridlist: list[str]
|
||||
|
||||
|
||||
class GetCardRecord(BaseSchema):
|
||||
userid: str
|
||||
groupname: str
|
||||
checkin_type: str
|
||||
exception_type: str
|
||||
checkin_time: datetime.datetime
|
||||
location_title: str
|
||||
location_detail: str
|
||||
wifiname: str
|
||||
notes: str
|
||||
wifimac: str
|
||||
mediaids: list[str]
|
||||
sch_checkin_time: datetime.datetime
|
||||
groupid: int
|
||||
schedule_id: int
|
||||
timeline_id: int
|
||||
|
||||
|
||||
class GetCardRecordsResponse(BaseSchema):
|
||||
errcode: int
|
||||
errmsg: str
|
||||
checkindata: list[GetCardRecord]
|
||||
@@ -1,13 +1,13 @@
|
||||
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):
|
||||
"""
|
||||
创建部门
|
||||
|
||||
@param name: 部门名称。长度限制为1~32个字节,字符不能包括\:?”<>
|
||||
@param name: 部门名称。长度限制为1~32个字节,字符不能包括\\:?”<>
|
||||
@param name_en: 英文名称
|
||||
@param parentid: 父部门id。根部门id为1
|
||||
@param order: 在父部门中的次序值。order值小的排序靠前。
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from wecom.schemas.base import BaseSchema
|
||||
from service.wecom.schemas.base import BaseSchema
|
||||
|
||||
|
||||
class AccessTokenParams(BaseSchema):
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
46
uv.lock
generated
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user