[build-system] requires = ["setuptools>=61.0", "wheel"] build-backend = "setuptools.build_meta" [project] name = "whoop-mcp" version = "0.8.5" authors = [ {name = "Ashwanthram K L", email = "k.l.ashwanthram@gmail.com"} ] description = "Read-only WHOOP v2 API access for MCP clients — direct OAuth, local SQLite cache, CSV/JSONL/Parquet exports" readme = "README.md" license = {text = "MIT"} requires-python = ">=3.10" classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Topic :: Scientific/Engineering :: Information Analysis" ] keywords = [ "whoop", "fitness", "mcp", "claude", "api", "health", "wearable", "recovery", "sleep", "workout" ] dependencies = [ "mcp>=1.0.0", "httpx>=0.25.0", "pydantic>=2.0.0", "python-dotenv>=1.0.0", "cryptography>=41.0.0", "typing-extensions>=4.0.0", "requests>=2.28.0" ] [project.optional-dependencies] # Install with `pip install whoop-mcp[dev]` — mirrors requirements-dev.txt. dev = [ "pytest>=7.0.0", "pytest-asyncio>=0.21.0", "respx>=0.21.0", "freezegun>=1.2.0", "ruff>=0.5.0", "mypy>=1.10.0", "build>=0.10.0", "twine>=4.0.0", "pre-commit>=3.5.0", ] [project.urls] Homepage = "https://github.com/AshwanthramKL/whoop-mcp" Documentation = "https://github.com/AshwanthramKL/whoop-mcp#readme" Repository = "https://github.com/AshwanthramKL/whoop-mcp" "Bug Reports" = "https://github.com/AshwanthramKL/whoop-mcp/issues" Changelog = "https://github.com/AshwanthramKL/whoop-mcp/blob/main/CHANGELOG.md" [project.scripts] whoop-mcp = "whoop_mcp_server:main" whoop-mcp-oauth = "setup_direct_oauth:main" whoop-mcp-install-skills = "whoop_install_skills:main" [tool.setuptools] package-dir = {"" = "src"} py-modules = [ "__version__", "auth_manager", "config", "setup_direct_oauth", "whoop_client", "whoop_export", "whoop_install_skills", "whoop_logging", "whoop_mcp_server", "whoop_models", "whoop_store", "whoop_sync", ] [tool.ruff] line-length = 100 target-version = "py310" extend-exclude = [".venv", "build", "dist", "*.egg-info"] [tool.ruff.lint] # Lean rule set: correctness-biased, not style-policing. # E/W: pycodestyle, F: pyflakes, I: isort, UP: pyupgrade, B: bugbear, # SIM: simplify, RUF: ruff-native. Skip D (docstrings) and ANN (annotations) # — we care about correctness and readability, not doc coverage policing. select = ["E", "W", "F", "I", "UP", "B", "SIM", "RUF"] ignore = [ "E501", # line too long — handled by the formatter, not the linter "B008", # allow Pydantic `Field(...)` as default arg (idiomatic) "SIM105", # allow try/except/pass where `contextlib.suppress` is noisier ] [tool.ruff.lint.per-file-ignores] "tests/*" = ["B011"] # allow asserts in tests "tests/record_fixtures.py" = ["T201"] # allow print() in the fixture recorder [tool.ruff.format] quote-style = "double" indent-style = "space" line-ending = "lf" [tool.mypy] python_version = "3.10" mypy_path = ["src"] # Practical-strict: catch the bugs that matter, don't police every annotation. # warn_return_any is off because most of our returned shapes come from # json-loaded dicts (typed Any) and fighting that just noises out the real # issues; turn it back on once we type Store return shapes with TypedDict. warn_unused_configs = true check_untyped_defs = true no_implicit_optional = true warn_redundant_casts = true warn_unreachable = true strict_equality = true [[tool.mypy.overrides]] module = ["tests.*"] disallow_untyped_defs = false check_untyped_defs = false [[tool.mypy.overrides]] # Third-party modules without type stubs. module = [ "requests", "respx", "freezegun", "pyarrow", "pyarrow.*", "mcp", "mcp.*", ] ignore_missing_imports = true [tool.coverage.run] source = ["src"] omit = [ "*/tests/*", "*/test_*.py" ] [tool.coverage.report] exclude_lines = [ "pragma: no cover", "def __repr__", "raise AssertionError", "raise NotImplementedError", "if __name__ == .__main__.:" ]