[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "trackstarr" dynamic = ["version"] description = "Keep media library audio and subtitle tracks tidy" readme = "README.md" license = { file = "LICENSE" } # What the Alpine image ships and the only version CI tests. requires-python = ">=3.14" authors = [{ name = "MarcedForLife" }] keywords = ["ffmpeg", "media", "radarr", "sonarr", "plex", "transcoding"] classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Topic :: Multimedia :: Video", ] # One runtime dependency; the bar for a second is high. cryptography seals the # credentials in settings.json, which needs an AEAD cipher the stdlib lacks. # The floor is the first release with musllinux abi3 wheels for Alpine. dependencies = ["cryptography>=45"] [project.scripts] trackstarr = "trackstarr.cli:main" [project.urls] Homepage = "https://github.com/MarcedForLife/trackstarr" Issues = "https://github.com/MarcedForLife/trackstarr/issues" # A group, so the tooling stays out of the published metadata and `uv sync` # installs it. Loose bounds: uv.lock pins the versions CI installs. [dependency-groups] dev = ["mypy>=2", "pytest>=8", "pytest-cov>=5", "ruff>=0.14"] [tool.hatch.version] path = "src/trackstarr/__init__.py" [tool.hatch.build.targets.wheel] packages = ["src/trackstarr"] [tool.pytest.ini_options] testpaths = ["tests"] filterwarnings = ["error"] # strict-markers makes a mark typo an error; strict-config likewise for this # table. addopts = ["--strict-markers", "--strict-config"] # --cov is not in addopts: it slows every run and the in-image suite has no # pytest-cov. `pytest --cov` picks the settings up from here. [tool.coverage.run] source_pkgs = ["trackstarr"] branch = true # The `python -m trackstarr` shim: two imports and a call to cli.main. omit = ["*/trackstarr/__main__.py"] [tool.coverage.report] show_missing = true exclude_also = ["if __name__ == .__main__.:"] # 100, so a line arriving without a test fails the build the day it lands. # Untestable code takes an explicit `# pragma: no cover` with a reason; so far # every one is a thread body or startup wiring. `grep -rn "pragma: no cover" # src` is the list. fail_under = 100 # src only; type-checking the tests buys little. Not --strict, which wants a # type for every ffprobe dict, whose shape is the thing being interrogated. # These are the strict flags that pass clean today. [tool.mypy] files = ["src"] warn_unused_ignores = true warn_redundant_casts = true warn_unreachable = true no_implicit_reexport = true strict_equality = true [tool.ruff] line-length = 96 src = ["src", "tests"] [tool.ruff.lint] select = [ "E", "W", # pycodestyle "F", # pyflakes "I", # isort "B", # bugbear "UP", # pyupgrade "N", # pep8-naming "SIM", # flake8-simplify "S", # bandit "A", # builtin shadowing "C4", # comprehensions "DTZ", # naive datetimes "FURB", # refurb, modern idiom "G", # logging call format "ISC", # implicit string concatenation, i.e. the comma you forgot "LOG", # logging misuse "PERF", # obviously slow constructs "PIE", # assorted lint "RET", # return flow "SLOT", "TID", # tidy imports "PLC", "PLE", # pylint's conventions and errors "RUF", ] # Unselected: TRY, PLW, PT, C90 and the PL refactor rules fire only on # deliberate code here and would buy a row of noqa comments. ANN and D # duplicate mypy and the docstrings. PTH would be a pathlib migration. ignore = [ # Subprocess calls are the point of this tool; the argument lists are built # from validated config, never through a shell. "S603", "S607", # 0.0.0.0 is the right default for a container's listener. "S104", # urllib only ever gets the configured Radarr/Sonarr base URL. "S310", ] [tool.ruff.lint.per-file-ignores] # S101: asserts are what tests are. S108: the planner tests' /tmp paths are # never opened. S105-S107: the "passwords" are fixture secrets. DTZ001: the # cron tests are about naive wall-clock matching. "tests/*" = ["S101", "S105", "S106", "S107", "S108", "DTZ001"] [tool.ruff.lint.isort] # conftest is a local test helper, not a third-party package. known-first-party = ["trackstarr", "conftest"]