[project]
name = "helix-server"
version = "0.1.0"
description = "Helix IT Platform - Backend"
requires-python = ">=3.12"
dependencies = [
    "fastapi>=0.141.1",
    "uvicorn[standard]>=0.34.0",
    "sqlalchemy[asyncio]>=2.0.36",
    "aiomysql>=0.3.2",
    "pymysql>=1.2.0",
    "alembic>=1.18.5",
    # Security floors for transitive dependencies are explicit so a future
    # lock refresh cannot silently reintroduce versions fixed by #2316.
    "Mako>=1.3.12",
    "pyasn1>=0.6.4",
    "urllib3>=2.7.0",
    "aiohttp>=3.14.1",
    "starlette>=1.3.1",
    "idna>=3.15",
    "pydantic>=2.10.0",
    "pydantic-settings>=2.14.2",
    "httpx>=0.28.0",
    "anthropic>=0.40.0",
    "PyJWT>=2.13.0",
    "cryptography>=48.0.1",
    "redis>=5.2.0",
    "python-multipart>=0.0.31",
    "openpyxl>=3.1.5",
    "bcrypt>=5.0.0",
    "pypinyin>=0.55.0",
    "boto3>=1.28.0",
    "tencentcloud-sdk-python>=3.0.0",
    "cos-python-sdk-v5>=1.9.29",
    "apscheduler>=3.10,<4",
    "google-api-python-client>=2.194.0",
    "google-auth>=2.49.2",
    # Explicit transitive deps of google-api-python-client used by
    # providers/gcp.py::_svc to attach a hard socket timeout. They
    # currently ship as transitive deps, but pinning here protects
    # against a future minor that drops httplib2 — without the pin
    # the AuthorizedHttp + Http(timeout=30) shim would AttributeError
    # at first call and kill every GCP sync (Round-2 review SS3 P2).
    "google-auth-httplib2>=0.2.0",
    "httplib2>=0.32.0",
    "google-cloud-bigquery>=3.20.0",
    "signxml>=3.2.2",
    "lxml>=5.3.0",
    "defusedxml>=0.7.1",
    "mcp>=1.28.1,<2",
    "Pillow>=12.3.0",
    "reportlab>=4.2.0",
    "geoip2>=4.8.0",
    "python-docx>=1.1.0",
    # AI Labs document attachments: the only text extractor in the tree that
    # reads PDFs. ``reportlab`` writes them and cannot read; ``python-docx`` /
    # ``openpyxl`` cover .docx / .xlsx, and PDF is the format employees
    # actually attach. Pure Python, no native build, so it costs the image
    # nothing but a wheel.
    "pypdf>=6.4.0",
    # Upper-bounded on purpose: ``holiday_source`` pattern-matches this package's
    # human-readable names to recover China's 调休 workdays, and vacanza reworks
    # those strings freely between minors. A lock refresh that changed them would
    # break makeup-day derivation with CI still green, so a major-version bump has
    # to be a deliberate edit that re-checks ``test_dataset_suggestion_*``.
    "holidays>=0.60,<1.0",
]

# NOTE: the official ``helix`` CLI used to live here as a
# ``[project.scripts]`` entry (``app.cli.main:main``). It was extracted
# into its own dependency-light distribution at repo-root ``cli/``
# (package ``helix_cli``, only depends on httpx) so it can be shipped to
# employees as a wheel / single-file binary independent of this backend.
# See docs/architecture/DECISIONS.md (CLI extraction ADR).

[project.optional-dependencies]
dev = [
    "pytest>=9.0.3",
    "pytest-asyncio>=0.25.0",
    "pytest-timeout>=2.4.0",
    "pytest-xdist[psutil]>=3.6.0",
    "httpx>=0.28.0",
    "respx>=0.22.0",
    "factory-boy>=3.3.0",
    "faker>=33.0.0",
    "pytest-cov>=6.0.0",
    "ruff>=0.8.0",
    "mypy>=1.13.0,<2",
    "Pygments>=2.20.0",
    # Drives the FastAPI lifespan in tests so the FastMCP session
    # manager is started for ``test_mcp_endpoint.py``.
    "asgi-lifespan>=2.1.0",
]

[tool.ruff]
target-version = "py312"
line-length = 100
# Applied migrations are immutable history: every file under
# ``alembic/versions`` has already run against production, so reformatting
# one buys nothing and makes the chain harder to audit. (The 102 findings
# there are 99 import-order nits plus two cosmetics in migrations 125/212.)
# The Alembic env/config module itself stays linted.
extend-exclude = ["alembic/versions"]

[tool.ruff.lint]
# This config is now a go-ci gate (`.github/workflows/go-ci.yml` → "Backend Lint").
# Until 2026-07-30 it existed only for local runs nobody ran, and the server
# had accumulated 616 violations — including four `F821` undefined names in
# annotations and a shadowed `pydantic.ValidationError` import that made an
# `except` clause in the asset bulk-intake path miss the error it was
# written to catch. Keep the gate green; add a rule to `ignore` only with a
# recorded reason, not to get past CI.
select = ["E", "F", "I", "N", "W", "UP"]
ignore = [
    "N811",  # constant imported as non-constant — vendor SDK aliasing
    "N818",  # exception name without Error suffix — AppException family
    "E501",  # line length — enforced by review, not the linter
    # ── Structural exemptions (deliberate, not debt) ──────────────────
    # E402: module-level import not at top. 88 sites, concentrated in the
    # big routers/services (cmdb/router.py, auth/router.py, cmdb/sync.py),
    # where a mid-module import is the documented way this codebase breaks
    # import cycles between a domain module and the coordinator/driver it
    # needs. Hoisting them would reintroduce the cycles; moving them into
    # functions is a separate refactor, not a lint fix.
    "E402",
    # N806: non-lowercase variable in function. The cloud drivers mirror
    # vendor SDK / API field names verbatim (`Bindings`, `RoleAssignmentId`,
    # `MAX_ROWS`-style local constants) so a reader can diff Helix code
    # against the provider's own reference without a mental rename.
    "N806",
    "N802",  # same reason, for SDK-mirroring method names
    "N803",  # same reason, for SDK-mirroring argument names
    "N801",  # same reason, for SDK-mirroring class names
    "N814",  # camelCase imported as constant — vendor SDK aliasing
    "N817",  # camelCase imported as acronym — vendor SDK aliasing
]

[tool.ruff.lint.per-file-ignores]
# ``seeded`` is a pytest fixture imported from a sibling test module and
# then taken as a test parameter — ruff reads the parameter as shadowing
# the import (F811). F811 stays ON everywhere else: on a test module it
# catches two tests sharing a name, where the first silently never runs.
"tests/test_finance/test_finance_read.py" = ["F811"]
# Same shape: ``no_llm`` / ``fake_chat`` are pytest fixtures imported from the
# sibling 转人工 module so the two files exercise the SAME doubles — a second
# copy would drift, and drifted doubles are how a card-copy bug survives a
# green suite.
"tests/test_assistant/test_handoff_ticket_number_2026_09_07.py" = ["F811"]

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
markers = [
    "live: smoke tests that hit real external APIs (sandbox credentials); skipped in CI, run manually",
]

[tool.mypy]
python_version = "3.12"
warn_return_any = true
warn_unused_configs = true
ignore_missing_imports = true

[tool.hatch.build.targets.wheel]
packages = ["app"]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[dependency-groups]
dev = [
    "openpyxl>=3.1.5",
    "pandas>=3.0.1",
]
