--- name: Python project rules description: Python conventions for the FINN/Eiendom MCP server applyTo: "**/*.py" --- # Python conventions ## Runtime * Python **3.12+**. * Project-local virtualenv at `.venv/` (created by `uv venv` or `python3.12 -m venv .venv`). * All commands run inside the activated venv. * Editable install: `uv pip install -e ".[dev]"` (or `pip install -e ".[dev]"`). * Never install packages globally; never use `sudo pip`; never mutate host Python. * Add new dependencies to `pyproject.toml` in the same change that uses them. ## Language * Use Python 3.12 syntax. Prefer `X | None` over `Optional[X]`, `list[int]` over `List[int]`, structural pattern matching where it actually helps. * **Type hints on every function signature**, including private helpers. `mypy --strict finn_eiendom` is the target. * Async-first for I/O. Sync code is fine for parsing, scoring, and cache access (SQLite). * Pydantic v2 for all structured domain models, with `model_config = ConfigDict(...)`. No v1 `class Config:` blocks. ## Prefer * Small, pure functions for parsing, normalization, and scoring. * Explicit return types and explicit exceptions. * Dependency injection for HTTP clients and DB connections in tests (pass `client` / `conn` as args; let services own the defaults). * Domain names from the PRD (`FinnAd`, `EiendomUnit`, `SimilarUnit`, `analyze_search`, `get_or_fetch_ad`). * `dataclass` for internal value objects that don't cross the API boundary; Pydantic for anything serialized or validated. ## Avoid * Global mutable state (module-level dicts as caches, etc.). The only allowed module-level state is configuration loaded from env in `config.py`. * Hardcoded URLs, credentials, paths, or magic numbers anywhere outside `config.py`. * `httpx` imports anywhere except `finn_eiendom/http.py`. * `sqlite3` imports anywhere except `finn_eiendom/cache.py`. * `BeautifulSoup` imports anywhere except `finn_eiendom/search.py` and `finn_eiendom/ad.py`. * `msgpack` imports anywhere except `finn_eiendom/eiendom_no.py`. * Scraping, scoring, cache, or HTTP fetching logic inside MCP tool or CLI command bodies. * Direct network calls in unit tests — use `respx` and fixtures. * `print()` for logging — use the `logging` module. stdio MCP server logs go to **stderr only**. * Bare `except:` or `except Exception: pass` — catch the specific exception or let it propagate. ## External fetches All external fetches must support: * Configurable request delay (`FINN_REQUEST_DELAY_SECONDS`, `EIENDOM_NO_REQUEST_DELAY_SECONDS`). * Cache lookup before fetch. * Retry on 5xx with exponential backoff (`1s, 2s, 4s`). * Graceful failure that returns `None` or empty rather than raising, when the caller can degrade. * Structured logging at INFO for success, WARNING for retry, ERROR for final failure. ## Best practices * **Single responsibility per function.** If a function name needs "and" to describe it, it's two functions. * **Function length:** aim for under 30 lines. Past 50 lines it's a code smell — extract helpers. * **Cyclomatic complexity:** if you've got more than 3 levels of nesting, the function wants splitting. * **Naming:** `get_or_fetch_ad`, not `process_ad`. Verbs for actions, nouns for data. Avoid abbreviations except those well-known in the domain (`url`, `ad`, `nok`). * **DRY:** if you write the same logic, regex, SQL, or format string twice, extract it. The decision table in `PRD.md` §17.2 tells you where it belongs. * **Comments explain WHY**, not WHAT. The code already says what. * **Errors are loud:** raise with actionable messages (`f"Unknown listing_status {status!r}; expected one of {VALID_STATUSES}"`). The MCP boundary wraps them as `{"error": True, ...}`. ## When uncertain about a library API Use the `context7` MCP server **before** writing code: 1. `context7:resolve-library-id` with the package name → canonical library ID. 2. `context7:query-docs` with that ID + focused topic. See `docs.instructions.md`. Don't guess from training memory — Pydantic, FastMCP, and Typer all change. ## Tooling * `ruff check .` — lint. Target Python 3.12. Active rules: `E F I UP B SIM`. * `ruff format .` — format. Line length 100. * `mypy --strict finn_eiendom` — type-check. * `pytest` — run the full suite.