mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 20:27:13 +01:00
screen_locker pushed manual workouts as "pc" and the workout app as "phone". A role constant collides the moment a second machine takes the same role, and after a reinstall the new install inherits the previous one's CRDT identity. Both halves now mint a uuid once and persist it: the PC in ~/.local/share/screen_locker/.device_id, the app under SharedPreferences 'crdt.nodeId' -- the same key every sibling app uses. The old constant survives as the legacy id and is passed to sync_log / syncLog, so devices/<legacy>/ is skipped as this device's own rather than re-downloaded and re-merged as a peer's on every tick. It drops to None once that path is reclaimed. Note the *read* loop in _workout_sync deliberately still merges every device including this one: the PC needs its own manual workouts in the merged view, so "skip own" would be wrong there. Only the push path and the HLC node id move. The test fixture redirecting the new id file is added in the same change that introduces the constant -- without it the first test that syncs mints a uuid into the real state dir and this machine's live sync identity is decided by whichever test ran first. Verified: ~/.local/share/screen_locker/ held no .device_id after a full suite run. Verified against the live backend, not just mocks: ran the real push_pc_workouts, which minted 57582b3b and pushed 9 records. In the RTDB the new device's log is a strict superset of both old paths -- 0 of devices/pc/'s 10 records and 0 of devices/phone/'s 9 are missing from it. Python: 907 tests, 100% branch coverage. workout_app: 313 tests, analyzer clean. Pins bumped to crdt-sync-v0.6.0 / crdt_sync_dart-v0.7.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GZH89WEqAjuJT8WZdqc7AP
238 lines
8.3 KiB
TOML
238 lines
8.3 KiB
TOML
[project]
|
|
name = "screen-locker"
|
|
version = "1.0.0"
|
|
description = "Tkinter/systemd screen locker with workout tracking, sick-day management, and wake-alarm integration"
|
|
requires-python = ">=3.10"
|
|
# gatelock: shared lock-window/HMAC backend, also used by diet_guard and wake_alarm
|
|
# crdt-sync: shared CRDT merge scheme + GitHub-Contents-API sync transport,
|
|
# also used by diet_guard; here for phone-workout GitHub sync (Workstream C)
|
|
dependencies = [
|
|
"gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.4.0#subdirectory=gatelock",
|
|
"crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.6.0#subdirectory=crdt-sync",
|
|
]
|
|
|
|
[project.scripts]
|
|
screen-locker-status = "screen_locker.status_view:main"
|
|
|
|
# Optional extras. The `mcp` group is ONLY needed by the read-only MCP server
|
|
# (screen_locker._mcp), which Claude Code spawns via a dedicated venv (see
|
|
# scripts/setup_mcp.sh / .mcp.json). It is deliberately NOT a core dependency
|
|
# so the systemd/CLI system-python path stays dependency-light and never has
|
|
# to import the MCP SDK.
|
|
[project.optional-dependencies]
|
|
mcp = ["mcp>=2,<3"]
|
|
|
|
[tool.setuptools.packages.find]
|
|
# This repo also holds stronglift_replacement/ (an unrelated Flutter app, not
|
|
# a Python package); without this, setuptools' automatic flat-layout
|
|
# discovery sees two top-level directories and refuses to guess which is the
|
|
# installable package.
|
|
include = ["screen_locker*"]
|
|
|
|
[tool.ruff]
|
|
target-version = "py310"
|
|
include = ["*.py", "**/*.py"]
|
|
exclude = [".git", ".venv", "__pycache__", "build", "dist", ".eggs"]
|
|
|
|
[tool.ruff.lint]
|
|
select = ["ALL"]
|
|
ignore = [
|
|
"D203",
|
|
"D213",
|
|
"COM812",
|
|
"ISC001",
|
|
"S603",
|
|
]
|
|
fixable = ["ALL"]
|
|
unfixable = []
|
|
|
|
[tool.ruff.lint.per-file-ignores]
|
|
"**/tests/**/*.py" = [
|
|
"ANN", "ARG", "D", "DTZ011", "E501", "FBT",
|
|
"PLC0415", "PLR0913", "PLR2004", "PTH", "RUF003", "S101", "SLF001",
|
|
]
|
|
"**/test_*.py" = [
|
|
"ANN", "ARG", "D", "DTZ011", "E501", "FBT",
|
|
"PLC0415", "PLR0913", "PLR2004", "PTH", "RUF003", "S101", "SLF001",
|
|
]
|
|
"screen_locker/_runnerup_verification.py" = ["S314"]
|
|
# URL is always built from the hardcoded https:// _WTTR_URL constant with
|
|
# only the (quoted) city interpolated — never user/attacker controlled.
|
|
# ruff's S310 can't verify that via static analysis of a variable URL, so
|
|
# it always flags urlopen() here regardless; a runtime scheme check was
|
|
# considered and rejected (it can't ever trigger, so it's just an untested
|
|
# branch and a crash risk in the lock's startup path for zero real benefit).
|
|
"screen_locker/_temperature.py" = ["S310"]
|
|
# PERF203 (try/except inside a loop) describes a retry loop exactly, and lands
|
|
# where its cost is irrelevant: each iteration makes a network call and then
|
|
# sleeps for seconds, so handler setup is noise. Hoisting the handler out of
|
|
# the loop would mean swallowing the error into a return value, which the
|
|
# repo's no-silent-failures rule rightly discourages.
|
|
"screen_locker/_sync_retry.py" = ["PERF203"]
|
|
"screen_locker/_shutdown_base.py" = ["SLF001"]
|
|
# run_status walks the weekly-status pipeline (weekly logic, RunnerUp scan,
|
|
# bonus, heat-skip) as a sequence of stages — genuinely branchy by design, same
|
|
# rationale as the validation-chain functions below; not a config-object case.
|
|
"screen_locker/_status.py" = ["C901", "PLR0912", "PLR0915", "SLF001", "T201"]
|
|
# _heat_skip.py opens a tkinter fullscreen window; tk's .attributes()/other
|
|
# calls take positional booleans (its API), which FBT003 always flags.
|
|
"screen_locker/_heat_skip.py" = ["FBT003"]
|
|
# StatusWindow.__init__ takes one arg per independently-mocked dependency
|
|
# (verifier_factory, temperature_fetcher, log_file, ...) — same
|
|
# dependency-injection rationale as _status_data.py's gather_status below.
|
|
"screen_locker/status_view.py" = ["PLC0415", "PLR0913", "SLF001", "T201"]
|
|
# The verify-and-credit flow split out of status_view.py: it deliberately
|
|
# drives the bare verifier's own mixin methods (_verify_phone_workout,
|
|
# _verify_runnerup_workout, _apply_workout_credit) — the same single-source-of
|
|
# -truth reuse status_view.py above is exempted for — and _make_bare_verifier
|
|
# imports ScreenLocker inside the function to avoid an import cycle.
|
|
"screen_locker/_status_view_verify.py" = ["PLC0415", "SLF001"]
|
|
# explain_lock_decision takes one arg per real file path + sick history, and
|
|
# returns early per lock-decision-chain stage (see module docstring) — both
|
|
# are the deliberate design, not something to collapse into a config object.
|
|
"screen_locker/_compliance_state.py" = ["C901", "PLR0911", "PLR0913"]
|
|
# gather_status takes one arg per real file path, all independently
|
|
# overridable for tests — same rationale as _compliance_state.py above.
|
|
"screen_locker/_status_data.py" = ["PLR0913"]
|
|
# validate_manual_workout returns early per failed evidence-form check (same
|
|
# validation-chain shape as _compliance_state.py's explain_lock_decision) —
|
|
# deliberate design, not something to collapse into a config object.
|
|
"screen_locker/_manual_workout.py" = ["C901", "PLR0911"]
|
|
# _verify_phone_workout returns early per verification-chain stage (clock
|
|
# check, sync, ADB, HTTP, then terminal statuses) — same shape as the two
|
|
# ignores above, not something to collapse into a config object.
|
|
"screen_locker/_phone_verification.py" = ["C901", "PLR0911"]
|
|
"scripts/check_file_length.py" = ["PTH123"]
|
|
|
|
[tool.ruff.lint.pydocstyle]
|
|
convention = "google"
|
|
|
|
[tool.ruff.lint.isort]
|
|
force-single-line = false
|
|
force-sort-within-sections = true
|
|
known-first-party = ["screen_locker"]
|
|
|
|
[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"
|
|
indent-style = "space"
|
|
skip-magic-trailing-comma = false
|
|
line-ending = "auto"
|
|
docstring-code-format = true
|
|
|
|
[tool.mypy]
|
|
python_version = "3.10"
|
|
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 = true
|
|
no_implicit_optional = true
|
|
warn_redundant_casts = true
|
|
warn_unused_ignores = true
|
|
warn_no_return = true
|
|
warn_unreachable = true
|
|
disallow_any_unimported = true
|
|
disallow_any_explicit = false
|
|
disallow_any_generics = true
|
|
disallow_subclassing_any = true
|
|
strict_equality = true
|
|
extra_checks = true
|
|
ignore_missing_imports = true
|
|
show_error_codes = true
|
|
color_output = true
|
|
exclude = [".venv/"]
|
|
|
|
[tool.pylint.main]
|
|
# The repo root on the path, so a file outside the package (scripts/) can be
|
|
# linted on its own. pylint otherwise infers the path from the files it is
|
|
# given, so `scripts/verify_screen_fits.py` resolved `screen_locker` only when
|
|
# a package file happened to be in the same batch -- passing or failing by
|
|
# what else was staged.
|
|
init-hook = "import sys; sys.path.insert(0, '.')"
|
|
analyse-fallback-blocks = true
|
|
persistent = true
|
|
jobs = 0
|
|
py-version = "3.10"
|
|
ignore = [".venv", "__pycache__"]
|
|
ignore-patterns = [".*\\.pyi$"]
|
|
|
|
[tool.pylint.messages_control]
|
|
enable = "all"
|
|
disable = []
|
|
|
|
[tool.pylint.design]
|
|
min-public-methods = 0
|
|
max-module-lines = 1000
|
|
max-attributes = 10
|
|
|
|
[tool.pylint.typecheck]
|
|
generated-members = [
|
|
".*\\.assert_called_once_with",
|
|
".*\\.assert_called_once",
|
|
".*\\.assert_called",
|
|
".*\\.assert_not_called",
|
|
".*\\.assert_any_call",
|
|
".*\\.call_args",
|
|
".*\\.call_args_list",
|
|
".*\\.call_count",
|
|
]
|
|
|
|
[tool.bandit]
|
|
exclude_dirs = ["tests", ".venv"]
|
|
|
|
[tool.pytest.ini_options]
|
|
testpaths = ["screen_locker/tests"]
|
|
python_files = ["test_*.py", "*_test.py"]
|
|
python_classes = ["Test*"]
|
|
python_functions = ["test_*"]
|
|
addopts = [
|
|
"-v",
|
|
"--strict-markers",
|
|
"--strict-config",
|
|
"-ra",
|
|
"--cov=screen_locker",
|
|
"--cov-branch",
|
|
"--cov-report=term-missing",
|
|
"--cov-report=lcov",
|
|
# A fixture-only plugin module, not conftest.py (already at the
|
|
# repo's 400-line-per-file cap) -- see its own docstring.
|
|
"-p",
|
|
"screen_locker.tests.test_sync_fixtures",
|
|
]
|
|
filterwarnings = [
|
|
"error",
|
|
"ignore::DeprecationWarning",
|
|
"default::pytest.PytestUnraisableExceptionWarning",
|
|
]
|
|
|
|
[tool.coverage.run]
|
|
source = ["screen_locker"]
|
|
branch = true
|
|
omit = [
|
|
"*/__pycache__/*",
|
|
"*/tests/*",
|
|
"*/.venv/*",
|
|
]
|
|
|
|
[tool.coverage.report]
|
|
fail_under = 100
|
|
show_missing = true
|
|
skip_covered = false
|
|
exclude_lines = [
|
|
"pragma: no cover",
|
|
"raise NotImplementedError",
|
|
"raise AssertionError",
|
|
"if TYPE_CHECKING:",
|
|
'if __name__ == "__main__":',
|
|
]
|
|
partial_branches = ["pragma: no branch"]
|