[build-system] requires = ["hatchling>=1.27"] build-backend = "hatchling.build" [project] name = "houndarr" dynamic = ["version"] description = "A focused, self-hosted companion for Radarr, Sonarr, Lidarr, Readarr, and Whisparr that automatically searches for missing media in polite, controlled batches." readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.13" authors = [{ name = "Andrea A. Venti Fuentes", email = "av1155.dev@gmail.com" }] keywords = ["radarr", "sonarr", "lidarr", "readarr", "whisparr", "media", "automation", "self-hosted"] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: End Users/Desktop", "License :: OSI Approved :: GNU Affero General Public License v3", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.13", "Topic :: Multimedia", ] dependencies = [ "fastapi>=0.135.3", "starlette>=1.3.1", "uvicorn[standard]>=0.44.0", "jinja2>=3.1.6", "markupsafe>=3.0.3", "aiosqlite>=0.22.1", "aiosqlitepool==1.0.0", "async-lru==2.3.0", "httpx>=0.28.1", "bcrypt>=5.0.0", "cryptography>=48.0.1", "itsdangerous>=2.2.0", "python-multipart>=0.0.31", "click>=8.3.2", "pydantic>=2.10.0", ] # PEP 735 dependency groups: developer-only deps that are not part of the # published package metadata. uv auto-installs the `dev` group on `uv sync`, # so collaborators get the full dev toolchain with a single command. # https://peps.python.org/pep-0735/ [dependency-groups] dev = [ "ruff>=0.15.10", "mypy>=1.20.1", "bandit[toml]>=1.9.4", "pytest>=9.0.3", "pytest-asyncio>=1.3.0", "pytest-cov>=7.1.0", "pytest-xdist>=3.8.0", "httpx>=0.28.1", "respx>=0.23.1", "anyio>=4.13.0", ] # Optional browser-e2e group: playwright is heavy (~80 MB of browser # binaries) and only the dedicated CI job and the marketing screenshot # script need it. Editor users can opt in with ``uv sync --group e2e`` # to silence the three ``reportMissingImports`` findings on # ``scripts/marketing/capture_screenshots.py`` and ``tests/e2e_browser/``; # default ``uv sync`` keeps the dev install lean. e2e = [ "playwright>=1.55.0", "pytest-playwright>=0.7.1", ] [project.scripts] houndarr = "houndarr.__main__:cli" # --------------------------------------------------------------------------- # Hatchling # --------------------------------------------------------------------------- # Use a tiny custom metadata hook (hatch_build.py) so the version stays sourced # from the VERSION file. The release workflow (release.yml, chart.yml, # version-check.yml) and the /bump command continue to treat VERSION as the # single source of truth. [tool.hatch.metadata.hooks.custom] path = "hatch_build.py" [tool.hatch.build.targets.wheel] packages = ["src/houndarr"] [tool.hatch.build.targets.sdist] include = [ "src/houndarr", "VERSION", "README.md", "LICENSE", "CHANGELOG.md", "hatch_build.py", "pyproject.toml", ] # --------------------------------------------------------------------------- # Ruff # --------------------------------------------------------------------------- [tool.ruff] src = ["src"] line-length = 100 target-version = "py313" [tool.ruff.lint] select = [ "E", # pycodestyle errors "W", # pycodestyle warnings "F", # pyflakes "I", # isort "B", # flake8-bugbear "C4", # flake8-comprehensions "UP", # pyupgrade "SIM", # flake8-simplify "ANN", # flake8-annotations (selected rules only) "S", # bandit security "N", # pep8-naming ] ignore = [ "ANN401", # use of Any "S101", # use of assert (needed in tests) "S104", # binding to all interfaces is intentional for a server application "B008", # do not perform function calls in default arguments (FastAPI Depends pattern) "SIM117", # nested with — aiosqlite async context managers cannot be combined ] [tool.ruff.lint.per-file-ignores] "tests/**/*.py" = ["S", "ANN"] "src/houndarr/__main__.py" = ["ANN"] [tool.ruff.lint.isort] known-first-party = ["houndarr"] # --------------------------------------------------------------------------- # Mypy # --------------------------------------------------------------------------- [tool.mypy] python_version = "3.13" mypy_path = "src" strict = true warn_return_any = true warn_unused_configs = true disallow_untyped_defs = true disallow_incomplete_defs = true check_untyped_defs = true disallow_untyped_decorators = false no_implicit_optional = true warn_redundant_casts = true warn_unused_ignores = true show_error_codes = true [[tool.mypy.overrides]] module = ["aiosqlite.*", "aiosqlitepool.*", "bcrypt.*"] ignore_missing_imports = true # --------------------------------------------------------------------------- # Pyright (editor LSP only; mypy --strict remains the CI gate) # --------------------------------------------------------------------------- # typeCheckingMode is pinned to "standard" so basedpyright matches upstream # pyright; basedpyright defaults to "all", which surfaces diagnostics that # mypy strict (the binding gate) does not enforce. [tool.pyright] include = ["src", "tests", "scripts"] extraPaths = ["."] pythonVersion = "3.13" venvPath = "." venv = ".venv" typeCheckingMode = "standard" # --------------------------------------------------------------------------- # Bandit (SAST) # --------------------------------------------------------------------------- [tool.bandit] exclude_dirs = ["tests", ".venv"] skips = [ "B101", # assert_used — needed in tests (excluded above) "B104", # hardcoded_bind_all_interfaces — intentional for a self-hosted server ] # --------------------------------------------------------------------------- # Pytest # --------------------------------------------------------------------------- [tool.pytest.ini_options] testpaths = ["tests"] # Browser e2e suite boots a live container and needs playwright installed; # skip it during the default pytest run and invoke explicitly with # ``pytest tests/e2e_browser/ --browser ...`` from the dedicated CI job. norecursedirs = ["e2e_browser"] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" addopts = "-q --tb=short" markers = [ "integration: end-to-end tests (live mock *arr containers or Playwright browser driving)", "pinning: characterization tests that lock current behaviour (run via `just pin`)", ]