Files
meida_backend/pyproject.toml
T
xsl cd0ddb131b feat(report): 报告 PDF 导出管线(Phase A,WeasyPrint 服务端渲染)
- POST /reports/{id}/export:request_key 幂等 + hd_export 日配额三段式(reserve/commit/refund)+ tier2 熔断场景闸(BudgetGuardDep),仅 succeeded 报告可导
- GET /reports/{id}/export/download:归属校验 + 路径收敛(rendered_asset_key 必须落在 report_export_dir 内),FileResponse 携带 UTF-8 文件名
- celery 任务 meida.report.export(cpu 队列):渲染→落盘 var/exports→回写 rendered_asset_key;业务性拒绝(报告状态变化)结案退款不重试,基础设施异常重试一次后兜底结案退款
- packages/report/pdf:Jinja2 模板 + WeasyPrint 渲染器(url_fetcher 全拒外部资源防 SSRF;维度中文名按 DIMENSION_BY_KEY 回填;色板维度渲染色卡)
- 单测 10 例(资格/幂等/并发同键/下载校验/渲染);scripts/seed_pdf_e2e.sql 供本地 E2E 造数
2026-08-30 13:17:23 +08:00

173 lines
5.9 KiB
TOML
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
[project]
name = "meida-backend"
version = "0.1.0"
description = "美搭 · 智能穿搭美学助手服务端"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.115",
"uvicorn[standard]>=0.32",
"pydantic>=2.9",
"pydantic-settings>=2.6",
"sqlalchemy[asyncio]>=2.0.36",
"asyncpg>=0.30",
"alembic>=1.14",
"pgvector>=0.3.6",
"redis>=5.2",
"celery>=5.4",
"httpx>=0.28",
"tenacity>=9.0",
"pyjwt[crypto]>=2.9",
"structlog>=24.4",
"orjson>=3.10",
"prometheus-client>=0.21",
"cos-python-sdk-v5>=1.9.44",
"tencentcloud-sdk-python-sms>=3.1.153",
"numpy>=2.5.2",
"weasyprint>=69.0",
"jinja2>=3.1.6",
]
[project.optional-dependencies]
spike = [
# 算法验证 SPIKE(T-P0-10/12/16)专用;模型文件在 models/(已 gitignore)
"onnxruntime>=1.20",
"opencv-python-headless>=4.10",
]
dev = [
"pytest>=8.3",
"pytest-asyncio>=0.24",
"pytest-cov>=6.0",
"respx>=0.22",
"ruff>=0.8",
"mypy>=1.13",
"import-linter>=2.1",
"detect-secrets>=1.5",
"pre-commit>=4.0",
]
[build-system]
requires = ["setuptools>=75"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
include = ["apps*", "packages*", "services*"]
# ── ruff ────────────────────────────────────────────────────────────────────
[tool.ruff]
line-length = 100
target-version = "py312"
extend-exclude = ["migrations/versions"]
[tool.ruff.lint]
select = ["E", "F", "I", "N", "UP", "B", "SIM", "ASYNC", "C4", "RUF"]
# B008 与 FastAPI 的 Depends() 默认值写法直接冲突
ignore = ["B008", "RUF001", "RUF002", "RUF003"]
[tool.ruff.lint.per-file-ignores]
"tests/**" = ["SLF001"]
# ── pytest ──────────────────────────────────────────────────────────────────
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
# 集成用例需要真实 PostgreSQL/Redis,默认不跑:`make test-integration` 显式开启。
# golden 用例(黄金集回归)同样需要真实 PG:CI 单独一步强制跑(T-P2-11),
# 本地默认跳过、`make golden` 显式开启。
addopts = "-q --strict-markers --strict-config -m 'not integration and not golden' --cov=packages --cov=apps"
markers = [
"integration: 需要真实 PostgreSQL / Redis(默认跳过)",
"golden: 黄金集回归(需要真实 PG;指标退化即失败,CI 强制跑)",
]
[tool.coverage.run]
branch = true
source = ["packages", "apps"]
omit = ["*/__init__.py"]
[tool.coverage.report]
show_missing = true
skip_covered = true
# ── mypy ────────────────────────────────────────────────────────────────────
[tool.mypy]
python_version = "3.12"
plugins = ["pydantic.mypy"]
ignore_missing_imports = true
warn_unused_configs = true
warn_redundant_casts = true
warn_unused_ignores = true
no_implicit_reexport = true
disallow_untyped_defs = true
check_untyped_defs = true
# 领域层与 LLM 网关是钱与正确性的所在,按 strict 要求;外层逐步收紧。
[[tool.mypy.overrides]]
module = ["packages.domain.*", "packages.llm.*"]
strict = true
[[tool.mypy.overrides]]
module = ["tests.*", "migrations.*"]
disallow_untyped_defs = false
# ── import-linter:分层依赖(架构文档 §4.2)────────────────────────────────
# 这些契约用机器代替 code review 来守住依赖方向。没有它,
# "domain 不许 import 框架" 这类约定会在第一次赶工时悄悄破掉。
[tool.importlinter]
root_packages = ["apps", "packages", "services"]
include_external_packages = true
[[tool.importlinter.contracts]]
name = "分层:apps → packages → domain,不得逆向"
type = "layers"
# 配额(billing)、账号(identity)、任务中心(task)是被各业务域共用的能力,
# 因此排在业务域下方一层 —— Agent 要预扣配额、报告要建任务,这些依赖是正当的。
# 反向不成立:billing 不该知道 Agent 的存在。
layers = [
"apps",
"packages.agent | packages.report | packages.tryon | packages.wardrobe",
"packages.identity | packages.billing | packages.task | packages.compliance | packages.notify | packages.analytics | packages.storage",
"packages.kb | packages.retrieval | packages.rules | packages.memory | packages.profile",
"packages.llm",
"packages.shared",
"packages.domain",
]
[[tool.importlinter.contracts]]
name = "domain 层不依赖任何框架与 I/O"
type = "forbidden"
source_modules = ["packages.domain"]
forbidden_modules = [
"fastapi",
"starlette",
"sqlalchemy",
"alembic",
"celery",
"redis",
"httpx",
"pydantic_settings",
]
[[tool.importlinter.contracts]]
name = "业务包不得绕过网关直连大模型"
type = "forbidden"
source_modules = [
"packages.agent",
"packages.report",
"packages.identity",
"packages.billing",
"packages.task",
"packages.compliance",
]
forbidden_modules = ["packages.llm.providers"]
# 只禁直接 import。经由 packages.llm.gateway 的间接依赖是正常的 ——
# 要拦的是"业务代码自己拼 HTTP 请求",那样成本归因与熔断都会被绕过。
allow_indirect_imports = "true"
[[tool.importlinter.contracts]]
name = "apps 之间互不依赖"
type = "independence"
modules = ["apps.api", "apps.worker", "apps.admin"]
# 例外:API 投递异步任务需要 worker 的任务签名。这是投递方向(api → worker),
# 反向不允许 —— worker 不得 import apps.api,否则 worker 会被迫加载整个 HTTP 层。
ignore_imports = ["apps.api.routers.tasks -> apps.worker.tasks"]