[project] name = "signal-bot" version = "0.2.1" description = "A Signal group bot: image commands and daily message tallies." readme = "README.md" license = "MIT" license-files = ["LICENSE"] requires-python = ">=3.14" # Exact pins, newest stable, per the dependency-freshness rule. A caret range # is "newest-compatible" by definition and gives the freshness gate nothing to # compare against. dependencies = [ "fastapi==0.141.1", "httpx==0.28.1", "python-dotenv==1.2.3", "PyYAML==6.0.3", "uvicorn==0.52.4", "websockets==17.1", ] [project.optional-dependencies] dev = [ "mypy==2.3.1", "pre-commit==4.6.2", "pytest==9.1.1", "pytest-asyncio==1.4.0", "pytest-cov==7.1.0", "ruff==0.16.5", "types-PyYAML==6.0.12.20260815", ] [project.scripts] signal-bot = "signal_bot.__main__:main" [build-system] requires = ["setuptools==84.0.0"] build-backend = "setuptools.build_meta" [tool.setuptools] packages = ["signal_bot", "fake_signal"] # ============================================================================ # RUFF # ============================================================================ [tool.ruff] target-version = "py314" line-length = 100 [tool.ruff.lint] select = ["ALL"] ignore = [ "D203", # conflicts with D211, we use D211 "D213", # conflicts with D212, we use D212 "COM812", # formatter owns trailing commas "ISC001", # formatter may create implicit string concatenation ] fixable = ["ALL"] # Verified empirically, not copied: with this block removed, ruff reports # exactly these four rules across tests/ and nothing else. SLF001 was in # the original list and fired zero times, so it is gone. [tool.ruff.lint.isort] # venv_bootstrap sets up .venv as a side effect of being imported, so it has # to be imported before anything that lives in .venv. Left to its own devices # isort sorts it alphabetically -- after fastapi and httpx -- which silently # breaks the thing it exists to do. Giving it its own section ahead of the # standard library makes the formatter enforce the order instead of undoing # it, which is better than a comment asking people not to reformat. section-order = [ "future", "bootstrap", "standard-library", "third-party", "first-party", "local-folder", ] [tool.ruff.lint.isort.sections] bootstrap = ["venv_bootstrap"] [tool.ruff.lint.per-file-ignores] # The single subprocess in this repository. S603 flags every subprocess call # and has no code-level fix -- exiftool is a binary, and the pure-Python EXIF # libraries handle neither HEIC nor MP4. signal_bot/external.py exists so this # entry can name exactly one file: any other module that shells out fails the # gate, which is the point. Do not widen this to a directory. "signal_bot/external.py" = ["S603"] # The bootstrap that installs .venv. It cannot route through external.py -- # that module lives inside the package it is installing -- so it is the one # other place allowed to start a process. Kept to a single small file for # exactly that reason. kuhy's call, 2026-08-31. Do not widen to a directory. "tool/venv_bootstrap.py" = ["S603", "S606"] "tests/**/*.py" = [ "ARG", # fixture and mock params exist for their side effect "D", # test names are the documentation "PLR2004", # `assert count == 2` beats a named constant "S101", # assert IS what tests do ] [tool.ruff.lint.pylint] # The test envelope builder takes 7 keyword-only fields, one per envelope # attribute. Splitting it into a dataclass would only move the count. max-args = 7 [tool.ruff.lint.pydocstyle] convention = "google" [tool.ruff.lint.flake8-quotes] docstring-quotes = "double" inline-quotes = "double" [tool.ruff.lint.flake8-tidy-imports] ban-relative-imports = "all" [tool.ruff.format] quote-style = "double" docstring-code-format = true # ============================================================================ # MYPY # ============================================================================ [tool.mypy] python_version = "3.14" strict = true warn_unreachable = true disallow_any_explicit = false # ============================================================================ # PYTEST / COVERAGE # ============================================================================ [tool.pytest.ini_options] testpaths = ["tests"] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" addopts = [ "--cov=signal_bot", "--cov=fake_signal", "--cov-branch", "--cov-report=term-missing", "--cov-fail-under=100", "--strict-markers", ] [tool.coverage.run] branch = true # No omits. Every line of both packages is covered by tests. [tool.coverage.report] # No "pragma: no cover" escape hatch: nothing in this repo uses one, and # leaving the pattern configured is an invitation to hide code from the # 100% gate later. TYPE_CHECKING bodies are genuinely never executed. exclude_lines = ["if TYPE_CHECKING:"] show_missing = true