screen-locker/pyproject.toml
Krzysztof kuhy Rudnicki 494cc74bb5
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
Add read-only MCP server exposing workout/lock status
Expose the screen-locker's compliance and lock-decision state as read-only
MCP tools (screen_locker._mcp) so Claude Code and its subagents can query
workout status without shelling out to the CLI or opening the Tk window.

- _mcp.py: FastMCP stdio server. Read-only tools get_status, get_summary,
  explain_lock, get_flags. Import leaf over stdout-free status functions
  (gather_status, format_summary_line, explain_lock_decision, predicates);
  logs to stderr. NO write/action tools by design — workouts are logged only
  from RunnerUp-verified TCX data, never from a caller's claim. No tool
  exposes the sync token or any HMAC key.
- .mcp.json + scripts/setup_mcp.sh: registration via a dedicated
  ~/.venvs/screen-locker-mcp interpreter; mcp is an optional-dep.
- 100% coverage of the new module; verified end-to-end over stdio.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQGQj8Ne6TriBExv91nG8v
2026-07-10 21:28:09 +02:00

193 lines
5.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
dependencies = [
"gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.1.0#subdirectory=gatelock",
]
[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>=1.9"]
[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"]
"screen_locker/_shutdown_base.py" = ["SLF001"]
"screen_locker/_status.py" = ["PLR0915", "SLF001", "T201"]
"screen_locker/status_view.py" = ["PLC0415", "SLF001", "T201"]
# 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"]
"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]
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",
]
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"]