modify: 集成文件上传
This commit is contained in:
+4
-1
@@ -4,6 +4,8 @@ app.version = "chongming"
|
||||
app.description = "基于 FastAPI 的 chongming 打包脚手架"
|
||||
app.debug = false
|
||||
env = "development"
|
||||
prefix = "/api/v1"
|
||||
upload_path = "uploads"
|
||||
|
||||
[database]
|
||||
type = "sqlite"
|
||||
@@ -12,6 +14,7 @@ generate_schemas = true # 生产环境使用迁移工具
|
||||
|
||||
[database.sqlite]
|
||||
echo = true
|
||||
echo_pool = true
|
||||
pool_size = 20
|
||||
max_overflow = 10
|
||||
pool_timeout = 30
|
||||
@@ -82,4 +85,4 @@ path = ["resources/app.mbank", "resources/plugins.mbank"]
|
||||
|
||||
[production.file_system]
|
||||
type = "svfs"
|
||||
path = "resources/application.vfs"
|
||||
path = "static.svfs"
|
||||
|
||||
+5
-2
@@ -4,6 +4,8 @@ app.version = "0.1.0"
|
||||
app.description = "基于 FastAPI 的 chongming 打包脚手架"
|
||||
app.debug = false
|
||||
env = "production"
|
||||
prefix = "/api/v1"
|
||||
upload_path = "uploads"
|
||||
|
||||
[database]
|
||||
type = "sqlite"
|
||||
@@ -11,7 +13,8 @@ url = "sqlite+aiosqlite:///./database.db"
|
||||
generate_schemas = true # 生产环境使用迁移工具
|
||||
|
||||
[database.sqlite]
|
||||
echo = true
|
||||
echo = false
|
||||
echo_pool = false
|
||||
pool_size = 20
|
||||
max_overflow = 10
|
||||
pool_timeout = 30
|
||||
@@ -82,4 +85,4 @@ path = ["resources/app.mbank", "resources/plugins.mbank"]
|
||||
|
||||
[production.file_system]
|
||||
type = "svfs"
|
||||
path = "resources/application.vfs"
|
||||
path = "static.svfs"
|
||||
|
||||
@@ -27,6 +27,7 @@ def app_run(app_config: dict, default_config: dict):
|
||||
server_config = app_config.get("server", {})
|
||||
host = server_config.get("host", "0.0.0.0")
|
||||
port = server_config.get("port", 8000)
|
||||
timeout_keep_alive = server_config.get("timeout_keep_alive", 5)
|
||||
|
||||
# 打包环境强制禁用重载
|
||||
is_frozen = getattr(sys, "frozen", False)
|
||||
@@ -54,6 +55,7 @@ def app_run(app_config: dict, default_config: dict):
|
||||
workers=workers,
|
||||
log_level="debug" if app_config.get("debug", False) else "info",
|
||||
access_log=True,
|
||||
timeout_keep_alive=timeout_keep_alive,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ description = "基于 modules-bank 的 fastapi 打包脚手架"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.8"
|
||||
dependencies = [
|
||||
"aiofiles>=24.1.0",
|
||||
"aiosqlite",
|
||||
"apscheduler",
|
||||
"bcrypt==4.0.1",
|
||||
@@ -17,6 +18,7 @@ dependencies = [
|
||||
"module-bank",
|
||||
"passlib[bcrypt]",
|
||||
"pyjwt",
|
||||
"python-multipart>=0.0.20",
|
||||
"sqlite-vfs",
|
||||
"sqlmodel[aiosqlite]",
|
||||
"tomli",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from sqlmodel import SQLModel, text
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
||||
from sqlalchemy.exc import OperationalError
|
||||
@@ -26,6 +27,7 @@ class TaskRequest(BaseModel):
|
||||
config = get_config()
|
||||
logger = get_logger("app")
|
||||
|
||||
|
||||
try:
|
||||
env = config["default"]["env"]
|
||||
except KeyError: # pragma: no cover
|
||||
@@ -70,6 +72,7 @@ async def lifespan(app: FastAPI):
|
||||
if "already exists" in str(e):
|
||||
raise
|
||||
|
||||
# 启动时:初始化任务服务
|
||||
task_service = get_task_service_instance(async_session_maker)
|
||||
app.state.task_service = task_service
|
||||
await task_service.start(init_tasks_callback)
|
||||
@@ -82,9 +85,8 @@ async def lifespan(app: FastAPI):
|
||||
get_cache().close()
|
||||
|
||||
|
||||
cors = config[env].get("cors", {})
|
||||
|
||||
# 创建 FastAPI 应用
|
||||
cors = config[env].get("cors", {})
|
||||
app = FastAPI(
|
||||
title=config["default"]["app"]["name"],
|
||||
version=config["default"]["app"]["version"],
|
||||
@@ -94,9 +96,12 @@ app = FastAPI(
|
||||
openapi_url="/openapi.json" if config[env]["debug"] else None,
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
# 添加 CORS 中间件
|
||||
app.add_middleware(CORSMiddleware, **cors)
|
||||
app.include_router(api_router, prefix="/api/v1")
|
||||
|
||||
# 添加路由
|
||||
app.include_router(api_router, prefix=config["default"]["prefix"])
|
||||
|
||||
|
||||
# 根路由
|
||||
@@ -107,7 +112,7 @@ async def root():
|
||||
"message": f"欢迎使用 {config['default']['app']['name']}",
|
||||
"version": config["default"]["app"]["version"],
|
||||
"docs": "/docs" if config[env]["debug"] else None,
|
||||
"health": "/api/v1/health",
|
||||
"health": f"{config['default']['prefix']}/health",
|
||||
}
|
||||
|
||||
|
||||
@@ -124,10 +129,19 @@ async def root():
|
||||
# )
|
||||
# return {"status": "success", "job_id": job.id}
|
||||
|
||||
# 挂载静态文件
|
||||
config = get_config()
|
||||
IMAGE_DIR = Path(f"./{config['default']['upload_path']}/images")
|
||||
app.mount(
|
||||
f"{config['default']['prefix']}/images",
|
||||
StaticFiles(directory=f"./{config['default']['upload_path']}/images"),
|
||||
name="images",
|
||||
)
|
||||
|
||||
if os.path.exists("static.svfs"):
|
||||
# 挂载前端静态文件
|
||||
if vfs_db_path := config[env].get("file_system", {}).get("path", None):
|
||||
logger.info("静态文件已存在,将使用静态文件服务")
|
||||
static_files_handler = SVFSStaticFiles(directory="/")
|
||||
static_files_handler = SVFSStaticFiles(directory="/", vfs_db_path=vfs_db_path)
|
||||
app.mount("/static", static_files_handler, name="static")
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ API 路由
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .routers import todo, auth
|
||||
from .routers import todo, auth, upload
|
||||
|
||||
# 创建主路由
|
||||
api_router = APIRouter()
|
||||
@@ -12,6 +12,7 @@ api_router = APIRouter()
|
||||
# 注册子路由
|
||||
api_router.include_router(todo.router, prefix="/todos", tags=["todo"])
|
||||
api_router.include_router(auth.router, prefix="/auth", tags=["authentication"])
|
||||
api_router.include_router(upload.router, prefix="/upload", tags=["upload"])
|
||||
|
||||
|
||||
# 导出路由
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -113,7 +115,7 @@ async def get_current_user_info(
|
||||
current_user: TokenData = Depends(get_current_user),
|
||||
):
|
||||
"""获取当前登录用户的信息"""
|
||||
user = await UserService.get_user_by_id(int(current_user.user_id), session)
|
||||
user = await UserService.get_user_by_id(UUID(current_user.user_id), session)
|
||||
if not user:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, UploadFile, File, HTTPException, Depends
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ..deps import get_current_user
|
||||
from ...service.uploads import UploadService
|
||||
from ...core.config import get_config
|
||||
|
||||
|
||||
config = get_config()
|
||||
router = APIRouter(dependencies=[Depends(get_current_user)])
|
||||
upload_image_service = UploadService(
|
||||
config["default"]["upload_path"], 1024 * 1024, "images"
|
||||
)
|
||||
|
||||
|
||||
class UploadResponse(BaseModel):
|
||||
url: str
|
||||
filename: str
|
||||
file_type: str
|
||||
|
||||
|
||||
@router.post("/image", response_model=UploadResponse)
|
||||
async def upload_image(file: UploadFile = File(...)):
|
||||
|
||||
if not file.content_type or not file.content_type.startswith("image/"):
|
||||
raise HTTPException(status_code=400, detail="Invalid file type")
|
||||
if file.filename is None:
|
||||
raise HTTPException(status_code=400, detail="Invalid file name")
|
||||
ext = os.path.splitext(file.filename)[1].lower()
|
||||
if ext not in [".jpg", ".jpeg", ".png", ".gif", ".webp"]:
|
||||
raise HTTPException(status_code=400, detail="Invalid file type")
|
||||
url, filename = await upload_image_service.upload(file)
|
||||
return UploadResponse(url=url, filename=filename, file_type=ext)
|
||||
@@ -23,6 +23,8 @@ class DefaultApp(TypedDict):
|
||||
class Default(TypedDict):
|
||||
app: DefaultApp
|
||||
env: Literal["production", "development"]
|
||||
prefix: str
|
||||
upload_path: str
|
||||
|
||||
|
||||
class SqliteConnectionArgs(TypedDict, total=False):
|
||||
@@ -86,7 +88,7 @@ class ModuleSystem(TypedDict, total=False):
|
||||
|
||||
class FileSystem(TypedDict, total=False):
|
||||
type: str
|
||||
path: List[str]
|
||||
path: str
|
||||
|
||||
|
||||
class Env(TypedDict, total=False):
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
class PermissionConstant:
|
||||
"""
|
||||
权限常量类 - 按业务模块分组管理所有系统权限
|
||||
|
||||
该类定义了系统中各个业务模块的权限标识符,采用分层结构组织:
|
||||
- 每个业务域作为一个内部类(如 Sales, Purchase, Inventory 等)
|
||||
- 每个权限使用 "资源:操作" 格式定义
|
||||
- 支持 CRUD 操作及业务特定操作(如 approve, export 等)
|
||||
|
||||
使用示例:
|
||||
>>> Permission.Sales.ORDER_CREATE # 获取销售订单创建权限
|
||||
'sales_order:create'
|
||||
>>> Permission.get_all() # 获取所有权限列表
|
||||
['sales_order:create', 'sales_order:read', ...]
|
||||
"""
|
||||
|
||||
class Sales:
|
||||
"""
|
||||
销售域权限常量
|
||||
|
||||
包含销售相关的权限定义:
|
||||
- 销售订单管理(ORDER_*)
|
||||
- 销售出库管理(OUTBOUND_*)
|
||||
- 客户管理(CUSTOMER_*)
|
||||
|
||||
权限格式: sales_order:create, customer:export 等
|
||||
"""
|
||||
|
||||
ORDER_CREATE = "sales_order:create" # 创建销售订单
|
||||
ORDER_READ = "sales_order:read" # 读取销售订单
|
||||
ORDER_UPDATE = "sales_order:update" # 修改销售订单
|
||||
ORDER_DELETE = "sales_order:delete" # 删除销售订单
|
||||
|
||||
OUTBOUND_CREATE = "sales_outbound:create" # 创建销售出库
|
||||
OUTBOUND_READ = "sales_outbound:read" # 读取销售出库
|
||||
OUTBOUND_UPDATE = "sales_outbound:update" # 修改销售出库
|
||||
OUTBOUND_DELETE = "sales_outbound:delete" # 删除销售出库
|
||||
OUTBOUND_APPROVE = "sales_outbound:approve" # 审批销售出库
|
||||
|
||||
CUSTOMER_CREATE = "customer:create" # 创建客户
|
||||
CUSTOMER_READ = "customer:read" # 读取客户
|
||||
CUSTOMER_UPDATE = "customer:update" # 修改客户
|
||||
CUSTOMER_DELETE = "customer:delete" # 删除客户
|
||||
CUSTOMER_EXPORT = "customer:export" # 导出客户
|
||||
|
||||
class Purchase:
|
||||
"""
|
||||
采购域权限常量
|
||||
|
||||
包含采购相关的权限定义:
|
||||
- 采购订单管理(ORDER_*)
|
||||
- 采购入库管理(INBOUND_*),包含审批和取消操作
|
||||
- 供应商管理(SUPPLIER_*)
|
||||
|
||||
权限格式: purchase_order:create, supplier:update 等
|
||||
"""
|
||||
|
||||
ORDER_CREATE = "purchase_order:create" # 创建采购订单
|
||||
ORDER_READ = "purchase_order:read" # 读取采购订单
|
||||
ORDER_UPDATE = "purchase_order:update" # 更新采购订单
|
||||
ORDER_DELETE = "purchase_order:delete" # 删除采购订单
|
||||
|
||||
INBOUND_CREATE = "purchase_inbound:create" # 创建采购入库
|
||||
INBOUND_READ = "purchase_inbound:read" # 读取采购入库
|
||||
INBOUND_UPDATE = "purchase_inbound:update" # 更新采购入库
|
||||
INBOUND_DELETE = "purchase_inbound:delete" # 删除采购入库
|
||||
INBOUND_APPROVE = "purchase_inbound:approve" # 审批采购入库
|
||||
INBOUND_CANCEL = "purchase_inbound:cancel" # 取消采购入库
|
||||
|
||||
SUPPLIER_CREATE = "supplier:create" # 创建供应商
|
||||
SUPPLIER_READ = "supplier:read" # 读取供应商
|
||||
SUPPLIER_UPDATE = "supplier:update" # 更新供应商
|
||||
SUPPLIER_DELETE = "supplier:delete" # 删除供应商
|
||||
|
||||
class Inventory:
|
||||
"""
|
||||
库存域权限常量
|
||||
|
||||
包含库存管理相关的权限定义:
|
||||
- 库存管理(STOCK_*)
|
||||
- 仓库管理(WAREHOUSE_*)
|
||||
- 其他出入库单据(OTHER_INBOUND_*, OTHER_OUTBOUND_*)
|
||||
- 调拨单管理(TRANSFER_*)
|
||||
- 盘点单管理(STOCK_CHECK_*)
|
||||
|
||||
特点: 大部分单据支持审批流程(APPROVE 权限)
|
||||
权限格式: stock:read, transfer:approve 等
|
||||
"""
|
||||
|
||||
STOCK_CREATE = "stock:create" # 创建库存
|
||||
STOCK_READ = "stock:read" # 读取库存
|
||||
STOCK_UPDATE = "stock:update" # 更新库存
|
||||
STOCK_DELETE = "stock:delete" # 删除库存
|
||||
|
||||
WAREHOUSE_CREATE = "warehouse:create" # 创建仓库
|
||||
WAREHOUSE_READ = "warehouse:read" # 读取仓库
|
||||
WAREHOUSE_UPDATE = "warehouse:update" # 更新仓库
|
||||
WAREHOUSE_DELETE = "warehouse:delete" # 删除仓库
|
||||
|
||||
# 库存单据
|
||||
|
||||
OTHER_INBOUND_CREATE = "other_inbound:create" # 创建其他入库
|
||||
OTHER_INBOUND_READ = "other_inbound:read" # 读取其他入库
|
||||
OTHER_INBOUND_UPDATE = "other_inbound:update" # 更新其他入库
|
||||
OTHER_INBOUND_DELETE = "other_inbound:delete" # 删除其他入库
|
||||
OTHER_INBOUND_APPROVE = "other_inbound:approve" # 审核其他入库
|
||||
|
||||
OTHER_OUTBOUND_CREATE = "other_outbound:create" # 创建其他出库
|
||||
OTHER_OUTBOUND_READ = "other_outbound:read" # 读取其他出库
|
||||
OTHER_OUTBOUND_UPDATE = "other_outbound:update" # 更新其他出库
|
||||
OTHER_OUTBOUND_DELETE = "other_outbound:delete" # 删除其他出库
|
||||
OTHER_OUTBOUND_APPROVE = "other_outbound:approve" # 审核其他出库
|
||||
|
||||
TRANSFER_CREATE = "transfer:create" # 创建调拨
|
||||
TRANSFER_READ = "transfer:read" # 读取调拨
|
||||
TRANSFER_UPDATE = "transfer:update" # 更新调拨
|
||||
TRANSFER_DELETE = "transfer:delete" # 删除调拨
|
||||
TRANSFER_APPROVE = "transfer:approve" # 审核调拨
|
||||
|
||||
STOCK_CHECK_CREATE = "stock_check:create" # 创建盘点
|
||||
STOCK_CHECK_READ = "stock_check:read" # 读取盘点
|
||||
STOCK_CHECK_UPDATE = "stock_check:update" # 更新盘点
|
||||
STOCK_CHECK_DELETE = "stock_check:delete" # 删除盘点
|
||||
STOCK_CHECK_APPROVE = "stock_check:approve" # 审核盘点
|
||||
|
||||
class Product:
|
||||
"""
|
||||
产品主数据权限常量
|
||||
|
||||
包含产品和基础数据管理的权限定义:
|
||||
- 产品管理(PRODUCT_*),支持导入导出
|
||||
- 产品分类管理(CATEGORY_*)
|
||||
- 计量单位管理(UNIT_*)
|
||||
|
||||
权限格式: product:import, product_category:read 等
|
||||
"""
|
||||
|
||||
PRODUCT_CREATE = "product:create" # 创建产品
|
||||
PRODUCT_READ = "product:read" # 读取产品
|
||||
PRODUCT_UPDATE = "product:update" # 更新产品
|
||||
PRODUCT_DELETE = "product:delete" # 删除产品
|
||||
PRODUCT_IMPORT = "product:import" # 导入产品
|
||||
PRODUCT_EXPORT = "product:export" # 导出产品
|
||||
|
||||
CATEGORY_CREATE = "product_category:create" # 创建产品分类
|
||||
CATEGORY_READ = "product_category:read" # 读取产品分类
|
||||
CATEGORY_UPDATE = "product_category:update" # 更新产品分类
|
||||
CATEGORY_DELETE = "product_category:delete" # 删除产品分类
|
||||
|
||||
UNIT_CREATE = "unit:create" # 创建单位
|
||||
UNIT_READ = "unit:read" # 读取单位
|
||||
UNIT_UPDATE = "unit:update" # 更新单位
|
||||
UNIT_DELETE = "unit:delete" # 删除单位
|
||||
|
||||
class Finance:
|
||||
"""
|
||||
财务域权限常量
|
||||
|
||||
包含财务管理相关的权限定义:
|
||||
- 应收应付查询(RECEIVABLE_READ, PAYABLE_READ)
|
||||
- 付款管理(PAYMENT_*),包含审批流程
|
||||
- 发票管理(INVOICE_*)
|
||||
|
||||
注意: 财务域以只读权限为主,写操作需要审批
|
||||
权限格式: payment:approve, invoice:create 等
|
||||
"""
|
||||
|
||||
RECEIVABLE_READ = "receivable:read" # 读取应收应付
|
||||
PAYABLE_READ = "payable:read" # 读取应付
|
||||
PAYMENT_CREATE = "payment:create" # 创建付款
|
||||
PAYMENT_READ = "payment:read" # 读取付款
|
||||
PAYMENT_APPROVE = "payment:approve" # 审批付款
|
||||
INVOICE_CREATE = "invoice:create" # 创建发票
|
||||
INVOICE_READ = "invoice:read" # 读取发票
|
||||
|
||||
class Report:
|
||||
"""
|
||||
报表与分析权限常量
|
||||
|
||||
包含各类报表查看权限:
|
||||
- 销售报表(SALES_VIEW)
|
||||
- 采购报表(PURCHASE_VIEW)
|
||||
- 库存报表(STOCK_VIEW)
|
||||
- 财务报表(FINANCE_VIEW)
|
||||
- 仪表盘(DASHBOARD_VIEW)
|
||||
|
||||
特点: 所有权限均为只读查看权限
|
||||
权限格式: report:sales:view, dashboard:view 等
|
||||
"""
|
||||
|
||||
SALES_VIEW = "report_sales:view" # 销售报表
|
||||
PURCHASE_VIEW = "report_purchase:view" # 采购报表
|
||||
STOCK_VIEW = "report_stock:view" # 库存报表
|
||||
FINANCE_VIEW = "report_finance:view" # 财务报表
|
||||
DASHBOARD_VIEW = "dashboard:view" # 仪表盘
|
||||
|
||||
class System:
|
||||
"""
|
||||
系统管理权限常量
|
||||
|
||||
包含系统级管理功能的权限定义:
|
||||
- 人员管理(PERSONNEL_*)
|
||||
- 角色管理(ROLE_*),包含角色分配权限
|
||||
- 系统配置(CONFIG_*)
|
||||
- 审计日志(AUDIT_LOG_READ)
|
||||
- 数据备份恢复(BACKUP_*)
|
||||
|
||||
注意: 这些是最高权限级别的操作,应谨慎分配
|
||||
权限格式: role:assign, backup:restore 等
|
||||
"""
|
||||
|
||||
PERSONNEL_CREATE = "personnel:create" # 创建人员
|
||||
PERSONNEL_READ = "personnel:read" # 读取人员
|
||||
PERSONNEL_UPDATE = "personnel:update" # 更新人员
|
||||
PERSONNEL_DELETE = "personnel:delete" # 删除人员
|
||||
|
||||
ROLE_CREATE = "role:create" # 创建角色
|
||||
ROLE_READ = "role:read" # 读取角色
|
||||
ROLE_UPDATE = "role:update" # 更新角色
|
||||
ROLE_DELETE = "role:delete" # 删除角色
|
||||
ASSIGN_PERMISSION = "permission:assign" # 分配权限
|
||||
ASSIGN_ROLE = "role:assign" # 分配角色
|
||||
|
||||
CONFIG_READ = "system_config:read" # 读取系统配置
|
||||
CONFIG_UPDATE = "system_config:update" # 更新系统配置
|
||||
AUDIT_LOG_READ = "audit_log:read" # 读取审计日志
|
||||
BACKUP_CREATE = "backup:create" # 创建备份
|
||||
BACKUP_RESTORE = "backup:restore" # 恢复备份
|
||||
|
||||
@classmethod
|
||||
def get_all(cls):
|
||||
"""
|
||||
获取系统中定义的所有权限字符串列表
|
||||
|
||||
该方法通过反射机制自动收集所有内部类中定义的权限常量,
|
||||
用于数据库同步、权限初始化等场景。
|
||||
|
||||
工作流程:
|
||||
1. 遍历 Permission 类的所有属性
|
||||
2. 跳过私有属性和非类属性
|
||||
3. 对每个内部类,遍历其所有字符串类型的权限常量
|
||||
4. 过滤出符合 "resource:action" 格式的权限标识
|
||||
|
||||
Returns:
|
||||
list[str]: 所有权限字符串的列表,例如:
|
||||
[
|
||||
'sales_order:create',
|
||||
'sales_order:read',
|
||||
'customer:export',
|
||||
...
|
||||
]
|
||||
|
||||
使用示例:
|
||||
>>> all_perms = Permission.get_all()
|
||||
>>> len(all_perms) # 获取权限总数
|
||||
85
|
||||
>>> 'sales_order:create' in all_perms # 检查权限是否存在
|
||||
True
|
||||
|
||||
Note:
|
||||
- 该方法会自动排除以下划线开头的私有属性
|
||||
- 只收集包含冒号的字符串值,确保是有效的权限标识
|
||||
- 返回的列表顺序取决于 dir() 函数的返回顺序
|
||||
"""
|
||||
perms = []
|
||||
for attr_name in dir(cls):
|
||||
attr = getattr(cls, attr_name)
|
||||
if attr_name.startswith("_") or not hasattr(attr, "__dict__"):
|
||||
continue
|
||||
# 遍历内部类的属性
|
||||
for sub_attr in dir(attr):
|
||||
if sub_attr.startswith("_"):
|
||||
continue
|
||||
val = getattr(attr, sub_attr)
|
||||
if isinstance(val, str) and ":" in val:
|
||||
perms.append(val)
|
||||
return perms
|
||||
@@ -21,6 +21,7 @@ class ExcludeWatchFilesFilter(logging.Filter):
|
||||
"apscheduler",
|
||||
"sqlalchemy",
|
||||
"sqlitedict",
|
||||
"sqlalchemy.engine.Engine",
|
||||
)
|
||||
|
||||
# 重写过滤方法
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from .user import User
|
||||
from .role import Role
|
||||
from .role import Role, RoleUpdate
|
||||
from .permission import Permission
|
||||
from .relationship_table import UserRole, RolePermission
|
||||
from .todo import Todo
|
||||
@@ -8,6 +8,7 @@ from .todo import Todo
|
||||
__all__ = [
|
||||
"User",
|
||||
"Role",
|
||||
"RoleUpdate",
|
||||
"Permission",
|
||||
"UserRole",
|
||||
"RolePermission",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from typing import Optional, List, TYPE_CHECKING
|
||||
|
||||
from sqlmodel import SQLModel, Field, Relationship
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .relationship_table import UserRole, RolePermission
|
||||
|
||||
@@ -19,3 +20,8 @@ class Role(SQLModel, table=True):
|
||||
permissions: List["Permission"] = Relationship(
|
||||
back_populates="roles", link_model=RolePermission
|
||||
)
|
||||
|
||||
|
||||
class RoleUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
|
||||
@@ -112,7 +112,7 @@ class AuthService:
|
||||
user_id = validation["user_id"]
|
||||
|
||||
# 获取用户
|
||||
user = await UserService.get_user_by_id(int(user_id), session)
|
||||
user = await UserService.get_user_by_id(user_id, session)
|
||||
if not user:
|
||||
return None
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlmodel import select
|
||||
|
||||
from ..model import Permission, RolePermission, Role, User
|
||||
from .user import UserService
|
||||
from .role import RoleService
|
||||
|
||||
|
||||
class PermissionService:
|
||||
@classmethod
|
||||
async def create_permission(
|
||||
cls,
|
||||
session: AsyncSession,
|
||||
name: str,
|
||||
resource: str,
|
||||
action: str,
|
||||
description: Optional[str] = None,
|
||||
) -> Permission:
|
||||
permission = Permission(
|
||||
name=name, resource=resource, action=action, description=description
|
||||
)
|
||||
session.add(permission)
|
||||
await session.commit()
|
||||
await session.refresh(permission)
|
||||
return permission
|
||||
|
||||
@classmethod
|
||||
async def get_permission_by_id(
|
||||
cls, session: AsyncSession, permission_id: int
|
||||
) -> Optional[Permission]:
|
||||
return await session.get(Permission, permission_id)
|
||||
|
||||
@classmethod
|
||||
async def get_permission_by_name(
|
||||
cls, session: AsyncSession, name: str
|
||||
) -> Optional[Permission]:
|
||||
result = await session.execute(
|
||||
select(Permission).where(Permission.name == name)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@classmethod
|
||||
async def assign_permission_to_role(
|
||||
cls, session: AsyncSession, role_id: int, permission_id: int
|
||||
) -> RolePermission:
|
||||
role_permission = RolePermission(role_id=role_id, permission_id=permission_id)
|
||||
session.add(role_permission)
|
||||
await session.commit()
|
||||
await session.refresh(role_permission)
|
||||
return role_permission
|
||||
|
||||
@classmethod
|
||||
async def assign_permission_to_user(
|
||||
cls,
|
||||
session: AsyncSession,
|
||||
user_id: UUID,
|
||||
permission_id: int,
|
||||
) -> User:
|
||||
permission = await cls.get_permission_by_id(session, permission_id)
|
||||
if not permission:
|
||||
raise HTTPException(status_code=404, detail="Permission not found")
|
||||
user = await UserService.get_user_by_id(user_id, session)
|
||||
if user is None:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
role = await RoleService.get_role_by_name(session, str(user_id))
|
||||
if role is None:
|
||||
role = await RoleService.create_role(
|
||||
session, str(user_id), is_only_user=True
|
||||
)
|
||||
if role.id is None:
|
||||
raise HTTPException(status_code=500, detail="角色ID无效")
|
||||
if permission.id is None:
|
||||
raise HTTPException(status_code=500, detail="权限ID无效")
|
||||
await cls.assign_permission_to_role(session, role.id, permission.id)
|
||||
user.roles.append(role)
|
||||
await session.commit()
|
||||
await session.refresh(user)
|
||||
return user
|
||||
|
||||
@classmethod
|
||||
async def unbind_permission_from_role(
|
||||
cls, session: AsyncSession, role_id: int, permission_id: int
|
||||
) -> Role:
|
||||
role = await RoleService.get_role_by_id(session, role_id)
|
||||
if role is None:
|
||||
raise HTTPException(status_code=404, detail="Role not found")
|
||||
role.permissions = [p for p in role.permissions if p.id != permission_id]
|
||||
await session.commit()
|
||||
await session.refresh(role)
|
||||
return role
|
||||
|
||||
@classmethod
|
||||
async def unbind_permission_from_user(
|
||||
cls, session: AsyncSession, user_id: UUID, permission_id: int
|
||||
):
|
||||
user = await UserService.get_user_by_id(user_id, session)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
role = await RoleService.get_role_by_name(session, str(user.id))
|
||||
if role is None:
|
||||
raise HTTPException(status_code=404, detail="Role not found")
|
||||
role.permissions = [
|
||||
permission
|
||||
for permission in role.permissions
|
||||
if permission.id != permission_id
|
||||
]
|
||||
await session.commit()
|
||||
await session.refresh(role)
|
||||
@@ -0,0 +1,97 @@
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlmodel import select
|
||||
|
||||
from ..model import Role, RoleUpdate, UserRole
|
||||
from ..service.user import UserService
|
||||
|
||||
|
||||
class RoleService:
|
||||
@staticmethod
|
||||
async def create_role(
|
||||
session: AsyncSession,
|
||||
name: str,
|
||||
description: Optional[str] = None,
|
||||
is_only_user: bool = False,
|
||||
) -> Role:
|
||||
role = Role(name=name, description=description, is_only_user=is_only_user)
|
||||
session.add(role)
|
||||
await session.commit()
|
||||
await session.refresh(role)
|
||||
|
||||
return role
|
||||
|
||||
@staticmethod
|
||||
async def assign_role_to_user(
|
||||
session: AsyncSession,
|
||||
user_id: UUID,
|
||||
role_id: int,
|
||||
):
|
||||
user_role = UserRole(user_id=user_id, role_id=role_id)
|
||||
session.add(user_role)
|
||||
await session.commit()
|
||||
await session.refresh(user_role)
|
||||
|
||||
@classmethod
|
||||
async def get_role_by_name(cls, session: AsyncSession, name: str) -> Optional[Role]:
|
||||
result = await session.execute(select(Role).where(Role.name == name))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@classmethod
|
||||
async def get_role_by_id(
|
||||
cls, session: AsyncSession, role_id: int
|
||||
) -> Optional[Role]:
|
||||
result = await session.get(Role, role_id)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
async def unbind_role_from_user(
|
||||
session: AsyncSession,
|
||||
user_id: UUID,
|
||||
role_id: int,
|
||||
):
|
||||
user = await UserService.get_user_by_id(user_id, session)
|
||||
if user is None:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
user.roles = [role for role in user.roles if role.id != role_id]
|
||||
await session.commit()
|
||||
await session.refresh(user)
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
async def delete_role(
|
||||
role_id: int,
|
||||
session: AsyncSession,
|
||||
) -> bool:
|
||||
result = await session.execute(
|
||||
select(UserRole).where(UserRole.role_id == role_id)
|
||||
)
|
||||
if result.scalars().all():
|
||||
raise HTTPException(status_code=400, detail="Role is not empty")
|
||||
role = await RoleService.get_role_by_id(session, role_id)
|
||||
if role is None:
|
||||
raise HTTPException(status_code=404, detail="Role not found")
|
||||
await session.delete(role)
|
||||
await session.commit()
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
async def update_role(
|
||||
cls,
|
||||
role_id: int,
|
||||
role_update: RoleUpdate,
|
||||
session: AsyncSession,
|
||||
) -> Optional[Role]:
|
||||
role = await cls.get_role_by_id(session, role_id)
|
||||
if role is None:
|
||||
raise HTTPException(status_code=404, detail="Role not found")
|
||||
role.name = role_update.name if role_update.name else role.name
|
||||
role.description = (
|
||||
role_update.description if role_update.description else role.description
|
||||
)
|
||||
await session.commit()
|
||||
await session.refresh(role)
|
||||
return role
|
||||
@@ -0,0 +1,43 @@
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi import UploadFile
|
||||
import aiofiles
|
||||
|
||||
from ..core.config import get_config
|
||||
|
||||
|
||||
config = get_config()
|
||||
|
||||
|
||||
class UploadService:
|
||||
def __init__(self, upload_dir: str, chunk_size: int, file_type: str):
|
||||
self.upload_dir = upload_dir
|
||||
self.file_type = file_type
|
||||
self.chunk_size = chunk_size
|
||||
UPLOAD_DIR = Path(f"{self.upload_dir}/{self.file_type}")
|
||||
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
async def upload(self, file_data: UploadFile):
|
||||
if file_data.filename is None:
|
||||
raise HTTPException(status_code=400, detail="Invalid file name")
|
||||
ext = os.path.splitext(file_data.filename)[1].lower()
|
||||
new_filename = f"{uuid.uuid4().hex}{ext}"
|
||||
file_path = os.path.join(self.upload_dir, self.file_type, new_filename)
|
||||
try:
|
||||
async with aiofiles.open(file_path, "wb") as out_file:
|
||||
while chunk := await file_data.read(self.chunk_size):
|
||||
await out_file.write(chunk)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
await file_data.close()
|
||||
return (
|
||||
f"{config['default']['prefix']}/{self.file_type}/{new_filename}".replace(
|
||||
"\\", "/"
|
||||
),
|
||||
new_filename,
|
||||
)
|
||||
@@ -1,4 +1,5 @@
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -14,7 +15,7 @@ class UserService:
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@staticmethod
|
||||
async def get_user_by_id(user_id: int, session: AsyncSession) -> Optional[User]:
|
||||
async def get_user_by_id(user_id: UUID, session: AsyncSession) -> Optional[User]:
|
||||
return await session.get(User, user_id)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
from typing import Callable, Optional
|
||||
|
||||
from ..core.scheduler import TaskService
|
||||
from .execute_background import dev_init_admin
|
||||
from ..core.config import get_config
|
||||
from .dev_init_db import init_db
|
||||
|
||||
|
||||
config = get_config()
|
||||
|
||||
|
||||
async def init_tasks_callback(task_service: TaskService):
|
||||
if config["default"]["env"] == "development":
|
||||
|
||||
await task_service.add_date_job(
|
||||
dev_init_admin,
|
||||
init_db,
|
||||
run_date=None,
|
||||
args=[],
|
||||
job_id="task_name",
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
from ..core.scheduler import logger
|
||||
from ..core.scheduler import get_task_service_instance
|
||||
from ..core.constant import PermissionConstant
|
||||
from ..service.user import UserService
|
||||
from ..service.permission import PermissionService
|
||||
|
||||
|
||||
async def dev_init_admin(session):
|
||||
admin_user = await UserService.get_user_by_email("admin", session)
|
||||
if admin_user: # type: ignore
|
||||
logger.info("管理员用户已存在") # type: ignore
|
||||
return
|
||||
await UserService.create_user(session, "admin", "admin", is_superuser=True) # type: ignore
|
||||
|
||||
|
||||
async def init_permission(session):
|
||||
for permission in PermissionConstant.get_all():
|
||||
parts = permission.split(":", 1)
|
||||
resource = parts[0]
|
||||
action = parts[1] if len(parts) > 1 else ""
|
||||
|
||||
existing_permission = await PermissionService.get_permission_by_name(
|
||||
session, permission
|
||||
)
|
||||
if existing_permission:
|
||||
logger.debug(f"权限已存在: {permission}")
|
||||
continue
|
||||
|
||||
await PermissionService.create_permission(session, permission, resource, action)
|
||||
logger.info(f"创建权限: {permission}")
|
||||
|
||||
|
||||
async def init_db():
|
||||
task_service = get_task_service_instance()
|
||||
if task_service.async_session_maker is None:
|
||||
return
|
||||
async with task_service.async_session_maker() as session:
|
||||
await dev_init_admin(session)
|
||||
await init_permission(session)
|
||||
@@ -1,15 +0,0 @@
|
||||
from ..core.scheduler import logger
|
||||
from ..service.user import UserService
|
||||
from ..core.scheduler import get_task_service_instance
|
||||
|
||||
|
||||
async def dev_init_admin():
|
||||
task_service = get_task_service_instance()
|
||||
if task_service.async_session_maker is None:
|
||||
return
|
||||
async with task_service.async_session_maker() as session:
|
||||
admin_user = await UserService.get_user_by_email("admin", session)
|
||||
if admin_user: # type: ignore
|
||||
logger.info("管理员用户已存在") # type: ignore
|
||||
return
|
||||
await UserService.create_user(session, "admin", "admin", is_superuser=True) # type: ignore
|
||||
@@ -115,6 +115,8 @@ def run_pyinstaller():
|
||||
"--hidden-import",
|
||||
"aiosqlite",
|
||||
"--hidden-import",
|
||||
"aiofiles",
|
||||
"--hidden-import",
|
||||
"apscheduler",
|
||||
"--hidden-import",
|
||||
"diskcache",
|
||||
@@ -127,6 +129,8 @@ def run_pyinstaller():
|
||||
"--hidden-import",
|
||||
"sqlite_vfs",
|
||||
"--hidden-import",
|
||||
"python_multipart",
|
||||
"--hidden-import",
|
||||
"sqlite_vfs.core",
|
||||
"--hidden-import",
|
||||
"jwt",
|
||||
@@ -154,11 +158,12 @@ def run_pyinstaller():
|
||||
|
||||
async def init_database():
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from ..app.core.config import get_config
|
||||
from sqlmodel import SQLModel, text
|
||||
from sqlalchemy.exc import OperationalError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||
from ..app.service.user import UserService
|
||||
|
||||
from ..app.core.config import get_config
|
||||
from ..app.task.dev_init_db import init_permission, dev_init_admin
|
||||
|
||||
database = get_config()["database"]
|
||||
database_type = database["type"]
|
||||
@@ -187,7 +192,9 @@ async def init_database():
|
||||
raise
|
||||
# 添加管理员用户
|
||||
async with async_session_maker() as session:
|
||||
await UserService.create_user(session, "admin", "admin", is_superuser=True) # type: ignore
|
||||
# await UserService.create_user(session, "admin", "admin", is_superuser=True) # type: ignore
|
||||
await init_permission(session)
|
||||
await dev_init_admin(session)
|
||||
|
||||
|
||||
async def main():
|
||||
@@ -256,7 +263,6 @@ async def main():
|
||||
shutil.rmtree(r"build/pyarmor_runtime_000000", ignore_errors=True)
|
||||
os.remove(r"build/main.py")
|
||||
os.remove(r"build/server.py")
|
||||
# os.remove(r"build/config.toml")
|
||||
|
||||
print("构建完成")
|
||||
|
||||
|
||||
@@ -7,6 +7,31 @@ resolution-markers = [
|
||||
"python_full_version < '3.9'",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiofiles"
|
||||
version = "24.1.0"
|
||||
source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.9'",
|
||||
]
|
||||
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247, upload-time = "2024-06-24T11:02:03.584Z" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896, upload-time = "2024-06-24T11:02:01.529Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiofiles"
|
||||
version = "25.1.0"
|
||||
source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.10'",
|
||||
"python_full_version == '3.9.*'",
|
||||
]
|
||||
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2", size = 46354, upload-time = "2025-10-09T20:51:04.358Z" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668, upload-time = "2025-10-09T20:51:03.174Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiosqlite"
|
||||
version = "0.20.0"
|
||||
@@ -337,6 +362,8 @@ name = "chongming"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiofiles", version = "24.1.0", source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }, marker = "python_full_version < '3.9'" },
|
||||
{ name = "aiofiles", version = "25.1.0", source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }, marker = "python_full_version >= '3.9'" },
|
||||
{ name = "aiosqlite", version = "0.20.0", source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }, marker = "python_full_version < '3.9'" },
|
||||
{ name = "aiosqlite", version = "0.22.1", source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }, marker = "python_full_version >= '3.9'" },
|
||||
{ name = "apscheduler" },
|
||||
@@ -349,6 +376,8 @@ dependencies = [
|
||||
{ name = "passlib", extra = ["bcrypt"] },
|
||||
{ name = "pyjwt", version = "2.9.0", source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }, marker = "python_full_version < '3.9'" },
|
||||
{ name = "pyjwt", version = "2.11.0", source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }, marker = "python_full_version >= '3.9'" },
|
||||
{ name = "python-multipart", version = "0.0.20", source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }, marker = "python_full_version < '3.10'" },
|
||||
{ name = "python-multipart", version = "0.0.26", source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }, marker = "python_full_version >= '3.10'" },
|
||||
{ name = "sqlite-vfs" },
|
||||
{ name = "sqlmodel", version = "0.0.29", source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }, marker = "python_full_version < '3.9'" },
|
||||
{ name = "sqlmodel", version = "0.0.34", source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }, marker = "python_full_version == '3.9.*'" },
|
||||
@@ -370,6 +399,7 @@ dev = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "aiofiles", specifier = ">=24.1.0" },
|
||||
{ name = "aiosqlite" },
|
||||
{ name = "apscheduler" },
|
||||
{ name = "bcrypt", specifier = "==4.0.1" },
|
||||
@@ -378,6 +408,7 @@ requires-dist = [
|
||||
{ name = "module-bank" },
|
||||
{ name = "passlib", extras = ["bcrypt"] },
|
||||
{ name = "pyjwt" },
|
||||
{ name = "python-multipart", specifier = ">=0.0.20" },
|
||||
{ name = "sqlite-vfs" },
|
||||
{ name = "sqlmodel", extras = ["aiosqlite"] },
|
||||
{ name = "tomli" },
|
||||
@@ -1334,6 +1365,31 @@ wheels = [
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-multipart"
|
||||
version = "0.0.20"
|
||||
source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }
|
||||
resolution-markers = [
|
||||
"python_full_version == '3.9.*'",
|
||||
"python_full_version < '3.9'",
|
||||
]
|
||||
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-multipart"
|
||||
version = "0.0.26"
|
||||
source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.10'",
|
||||
]
|
||||
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pywin32-ctypes"
|
||||
version = "0.2.3"
|
||||
|
||||
Reference in New Issue
Block a user