[project] name = "ledgerlens-api" version = "1.0.0" description = "LedgerLens — Intelligent Document Processing & Financial Anomaly Detection Platform" requires-python = ">=3.12,<3.13" dependencies = [ "fastapi>=0.115,<1.0", "uvicorn[standard]>=0.32,<1.0", "pydantic>=2.9,<3.0", "pydantic-settings>=2.6,<3.0", "sqlalchemy[asyncio]>=2.0.36,<3.0", "asyncpg>=0.30,<1.0", "greenlet>=3.1,<4.0", "anthropic>=0.40", "pymupdf>=1.24,<2.0", "pandas>=2.2,<3.0", "rapidfuzz>=3.10,<4.0", "python-multipart>=0.0.12,<1.0", "slowapi>=0.1.9,<1.0", "httpx>=0.27,<1.0", ] [project.optional-dependencies] observability = ["langfuse>=2.53"] docgen = ["reportlab>=4.2,<5.0", "pillow>=10.4,<12.0"] # `scripts/deploy_space.py` only. Kept out of `dev` so CI installs neither the # client nor its transitive tree to run a test suite that never deploys anything. deploy = ["huggingface_hub>=0.28"] dev = [ "pytest>=8.3,<9.0", "pytest-asyncio>=0.24,<2.0", # Drives the app's lifespan (start-up/shut-down) around the API tests. "asgi-lifespan>=2.1,<3.0", # The test client. A transitive dependency of anthropic today, but the tests # import it directly, so it is declared directly. "httpx>=0.27,<1.0", "ruff>=0.8,<1.0", "mypy>=1.13,<2.0", "pandas-stubs>=2.2", "reportlab>=4.2,<5.0", "pillow>=10.4,<12.0", ] [build-system] requires = ["setuptools>=75"] build-backend = "setuptools.build_meta" [tool.setuptools.packages.find] include = ["app*"] # ---------------------------------------------------------------------------- # ruff — lint + format. Zero findings is a release gate (see AUDIT.md). # ---------------------------------------------------------------------------- [tool.ruff] line-length = 100 target-version = "py312" src = ["."] [tool.ruff.lint] select = [ "E", # pycodestyle errors "W", # pycodestyle warnings "F", # pyflakes "I", # isort "N", # pep8-naming "UP", # pyupgrade "B", # flake8-bugbear "A", # flake8-builtins "C4", # comprehensions "DTZ", # flake8-datetimez (naive datetimes are a real bug class here) "T20", # flake8-print (no stray prints in the service) "SIM", # flake8-simplify "PTH", # use pathlib "RUF", # ruff-specific "S", # bandit security "ASYNC", "ARG", # unused arguments "TID", # tidy imports ] ignore = [ "S101", # assert — used deliberately in tests and invariant checks ] [tool.ruff.lint.per-file-ignores] "tests/*" = ["S105", "S106", "ARG001", "ARG002"] # Corpus generation needs a *reproducible* PRNG, not a cryptographic one: # a fixed seed is the point, so the run is identical every time. "app/devtools/corpus.py" = ["S311"] # Operator-facing CLIs: their whole job is to print a report to the terminal. "scripts/*" = ["T201"] [tool.ruff.lint.isort] known-first-party = ["app"] # ---------------------------------------------------------------------------- # mypy — strict. Zero findings is a release gate. # ---------------------------------------------------------------------------- [tool.mypy] python_version = "3.12" strict = true warn_unreachable = true warn_unused_ignores = true disallow_any_generics = true no_implicit_reexport = true show_error_codes = true plugins = ["pydantic.mypy"] # PyMuPDF ships partial annotations; its constructors are untyped. Excluding it # from the untyped-call rule keeps `strict` on for every line we actually own. untyped_calls_exclude = ["pymupdf"] exclude = "^(\\.venv|build)/" [[tool.mypy.overrides]] module = [ "fitz.*", "pymupdf.*", "slowapi.*", "langfuse.*", "reportlab.*", "PIL.*", ] ignore_missing_imports = true [tool.pytest.ini_options] asyncio_mode = "auto" # The engine and its connection pool are session-scoped, so fixtures and tests # must share one event loop; a per-function loop would orphan pooled connections. asyncio_default_fixture_loop_scope = "session" asyncio_default_test_loop_scope = "session" testpaths = ["tests"] filterwarnings = [ # Our own code must not emit warnings — that is the "zero warnings" bar. "error", # PyMuPDF's SWIG bindings emit these three during module initialisation. # Promoting them to exceptions raises *inside* the C extension's init and # takes the interpreter down with SIGSEGV, so they are exempted by exact # message rather than by relaxing the rule. "ignore:builtin type swigvarlink has no __module__ attribute:DeprecationWarning", "ignore:builtin type SwigPyPacked has no __module__ attribute:DeprecationWarning", "ignore:builtin type SwigPyObject has no __module__ attribute:DeprecationWarning", ] addopts = "-q --strict-markers --strict-config" markers = [ "integration: requires a live Postgres (DATABASE_URL)", ]