signal-bot/pyproject.toml
Krzysztof kuhy Rudnicki aeb1c91b3c
Some checks are pending
CI / check (push) Waiting to run
release: 0.2.2 «Anterus» — the album, filled in, and video that can be added
Pope 19 of 267, 235-236.

The batch import landed: 174 files from a phone export became 115 new photos
in the album, deduplicated on exact bytes twice and stripped on the way in.
Every command has photos now, including one that had none at all and so
answered nobody who typed it.

Two things that had to be fixed before "strip on import" was even possible,
and both were live defects rather than new features:

media_tags documented that "-all=" zeroes the QuickTime date tags and allowed
them by value on that basis. Measured, it does not, so the positive check
downstream refused every mp4 the bot was ever offered. "-time:all=" is what
zeroes them.

And a tag allowlist cannot see a metadata *track*. An iPhone clip already in
the album carries mebx samples holding per-frame face bounding boxes and a
face id, which survive a complete tag strip because they live in mdat as
opaque payload with no tag to refuse. media_tracks drops the track and
verifies with ffprobe that only video and audio survived.

DOCS-changelog.md crossed the 250-line cap, so the ten most recent releases
stay and the rest move to DOCS-changelog-archive.md. Nothing reads the older
ones: announce caps a backlog at ten, and since() takes its floor from the
version ladder rather than from the file, so a group quiet for twenty
releases still gets the ten newest. Verified: a group last told 0.0.3 hears
exactly ten.

Not restarted. A bump is a publish -- restarting posts these bullets into
every watched group, including the real one -- so that is kuhy's to do.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WBrE1b9HvFw3xvG9bJiDFt
2026-08-31 13:48:01 +02:00

155 lines
4.9 KiB
TOML

[project]
name = "signal-bot"
version = "0.2.2"
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