This commit is contained in:
Ole
2026-05-16 06:54:17 +00:00
commit 1399f61c1a
44 changed files with 6746 additions and 0 deletions
@@ -0,0 +1,80 @@
---
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.