[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", ] [project.optional-dependencies] 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` 显式开启 addopts = "-q --strict-markers --strict-config -m 'not integration' --cov=packages --cov=apps" markers = ["integration: 需要真实 PostgreSQL / Redis(默认跳过)"] [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"]