[project] name = "endurain" version = "0.20.2" description = "Endurain API for the Endurain app" authors = [{name = "João Vitória Silva", email = "joao@endurain.com"}] # readme intentionally omitted — internal app, not published to PyPI requires-python = ">=3.13,<4.0" dependencies = [ "fastapi>=0.141.1", "uvicorn>=0.53.0", "python-dotenv>=1.2.3", "sqlalchemy>=2.0.54", "apscheduler>=3.11.3", "requests>=2.34.2", "stravalib>=2.6.0", "python-multipart>=0.0.32", "gpxpy>=1.6.2", "tcxreader>=0.4.11", "alembic>=1.20.0", "joserfc>=1.7.5", "fitdecode>=0.11.0", "numpy>=2.5.3", "geopy>=2.5.0", "pydantic-core>=2.46.5", "pydantic-settings>=2.15.0", "garminconnect>=0.3.16", "websockets>=17.1", "timezonefinder>=9.0.0", "psycopg[binary,pool]>=3.3.6", "user-agents>=2.2.0", "pydantic[email]>=2.13.5", "pyotp>=2.10.0", "qrcode[pil]>=8.2", "psutil>=7.2.2", "python-magic>=0.4.27", "pwdlib[argon2,bcrypt]>=0.3.1", "authlib>=1.8.0", "httpx2>=2.13.0", "itsdangerous>=2.2.0", "slowapi>=0.1.10", "safeuploads[fastapi]>=1.1.1", "nh3>=0.3.7", "aiofiles>=25.1.0", "staticmap>=0.5.7", # Pinned below the next minor: JASIL is pre-1.0 and documents breaking # changes on minor versions. "jasil[fastapi,geocoding,jobs,migrations]>=0.6.1,<0.7.0", ] [project.optional-dependencies] # Extra pulled in for multi-node (`distributed`) deployments: Redis-backed # shared state / event bus and S3-compatible object storage. The default image # (`uv export --no-dev`) ships neither, so `local` deployments stay lean. distributed = [ "boto3>=1.43.98", "redis>=8.1.0", "jasil[redis,s3]>=0.6.1,<0.7.0", ] [dependency-groups] typecheck = [ "mypy>=2.3.1", "types-requests", "types-psutil", "types-qrcode", "types-aiofiles", "types-Authlib", ] lint = [ "ruff>=0.16.8", ] test = [ "httpx2>=2.13.0", "faker>=40.39.0", "hypothesis>=6.168.0", "import-linter>=2.15", "pytest>=9.1.1", "pytest-asyncio>=1.4.0", "pytest-cov>=7.1.0", "boto3>=1.43.98", "redis>=8.1.0", ] dev = [ "hatch>=1.18.1", {include-group = "lint"}, {include-group = "test"}, {include-group = "typecheck"}, ] docs = [ "mkdocs-material>=9.7.7", "mkdocstrings[python]>=1.0.6", "mkdocs-redirects>=1.2.3", "mkdocs-swagger-ui-tag>=0.8.1", ] [tool.uv] required-version = "0.12.24" exclude-newer = "30 days" # jasil is first-party (endurain-project) and published through PyPI Trusted # Publishing with PEP 740 attestations, so it is exempt from the quarantine the # cutoff applies to third-party releases. Every other dependency keeps it. exclude-newer-package = { jasil = false } [[tool.uv.index]] name = "pypi" url = "https://pypi.org/simple" explicit = true [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = ["app"] [tool.hatch.build.targets.sdist] only-include = ["app"] [tool.hatch.envs.default] type = "virtual" path = ".venv" [tool.hatch.envs.default.scripts] lint = "ruff check . --fix" format = "ruff format ." lint-check = "ruff check ." format-check = "ruff format --check ." test = "pytest" typecheck = "mypy" validate = ["lint", "format", "test", "typecheck"] check = ["lint-check", "format-check", "test", "typecheck"] [tool.pytest.ini_options] pythonpath = ["app"] testpaths = ["tests"] python_files = ["test_*.py"] python_classes = ["Test*"] python_functions = ["test_*"] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" addopts = "--tb=short --strict-markers --cov --cov-report=term-missing" markers = [ "postgres: needs a disposable PostgreSQL database (TEST_DATABASE_URL); skipped when it is unset", "distributed: runs real API and worker processes against disposable PostgreSQL, Redis and S3 (TEST_DATABASE_URL, TEST_REDIS_URL, TEST_S3_ENDPOINT_URL); skipped unless all are set", ] filterwarnings = [ "ignore::DeprecationWarning:slowapi.*", ] [tool.coverage.run] source = ["app"] omit = ["*/tests/*", "*/__pycache__/*", "*/alembic/*", "*/migrations/*"] [tool.coverage.report] fail_under = 80 exclude_lines = [ "pragma: no cover", "def __repr__", "raise AssertionError", "raise NotImplementedError", "if __name__ == .__main__.:", "if TYPE_CHECKING:", ] [tool.mypy] python_version = "3.13" incremental = false check_untyped_defs = true mypy_path = ["app"] files = ["app"] exclude = ["tests/", "alembic/"] [[tool.mypy.overrides]] module = [ "apscheduler.*", "staticmap", "fitdecode", "garminconnect", "geopy.*", "tcxreader", "user_agents", "safeuploads.*", "boto3", "botocore.*", ] ignore_missing_imports = true [tool.ruff] target-version = "py313" line-length = 120 src = ["app", "tests"] [tool.ruff.lint] select = [ "E", # pycodestyle errors "W", # pycodestyle warnings "F", # pyflakes "I", # isort "N", # pep8-naming "UP", # pyupgrade "B", # flake8-bugbear "S", # flake8-bandit (security) "A", # flake8-builtins "T20", # flake8-print "SIM", # flake8-simplify "RUF", # ruff-specific rules "D", # pydocstyle; scoped to converted modules below, see per-file-ignores ] ignore = [ "E501", # line too long (handled by formatter) "B008", # do not perform function calls in argument defaults (FastAPI Depends/Query) — tracked in https://github.com/endurain-project/endurain/issues/653 ] [tool.ruff.lint.pydocstyle] convention = "google" [tool.ruff.lint.per-file-ignores] # Docstring rules ("D") apply to the converted modules (activities, followers). Everything # below is outside them until converted: removing a module from this list is part of # converting it. ``tests/architecture/test_module_boundaries.py`` keeps the module entries # in step with UNCONVERTED_MODULES. "app/api.py" = ["D"] "app/model_registry.py" = ["D"] "app/module_registry.py" = ["D"] "app/worker.py" = ["D"] "app/core/**/*.py" = ["D"] "app/data/**/*.py" = ["D"] # runtime data directories, not code "app/logs/**/*.py" = ["D"] "app/migrations/**/*.py" = ["D"] "app/modules/auth/**/*.py" = ["D"] "app/modules/garmin/**/*.py" = ["D"] "app/modules/gears/**/*.py" = ["D"] "app/modules/health/**/*.py" = ["D"] "app/modules/notifications/**/*.py" = ["D"] "app/modules/server_settings/**/*.py" = ["D"] "app/modules/strava/**/*.py" = ["D"] "app/modules/users/**/*.py" = ["D"] "app/modules/websocket/**/*.py" = ["D"] "scripts/**/*.py" = ["D"] "tests/**/*.py" = ["D", "S101", "S105", "S106", "S108"] # allow assert, password-like test data, and hardcoded /tmp-style paths in test mocks/fixtures "app/alembic/**/*.py" = ["D", "E402", "I001"] # alembic migrations have specific import order; docstrings not required "app/alembic/versions/v0_3_0_migration.py" = ["S105", "S106"] # legitimate password hashes in seed migration "app/modules/auth/identity_providers/public_router.py" = ["S105", "S106"] # false positive on token_type "app/modules/auth/schema.py" = ["S105", "S106"] # false positive on token_type Literal defaults "app/modules/auth/security.py" = ["S105", "S106"] # false positive on token_type string comparisons "app/modules/auth/utils.py" = ["S105", "S106"] # false positive on cookie name constants "app/main.py" = ["D", "S105"] # false positive on env var value [tool.ruff.format] quote-style = "double" indent-style = "space" line-ending = "auto"