property-shared 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. property_shared-0.1.0/.dockerignore +52 -0
  2. property_shared-0.1.0/.gitignore +252 -0
  3. property_shared-0.1.0/.gitlab-ci.yml +42 -0
  4. property_shared-0.1.0/CLAUDE.md +124 -0
  5. property_shared-0.1.0/Dockerfile +33 -0
  6. property_shared-0.1.0/GUIDELINES.md +55 -0
  7. property_shared-0.1.0/PKG-INFO +136 -0
  8. property_shared-0.1.0/PLAN.md +102 -0
  9. property_shared-0.1.0/README.md +107 -0
  10. property_shared-0.1.0/USER_GUIDE.md +331 -0
  11. property_shared-0.1.0/apify_actors/planning_scraper/.actor/actor.json +16 -0
  12. property_shared-0.1.0/apify_actors/planning_scraper/Dockerfile +14 -0
  13. property_shared-0.1.0/apify_actors/planning_scraper/README.md +118 -0
  14. property_shared-0.1.0/apify_actors/planning_scraper/input_schema.json +73 -0
  15. property_shared-0.1.0/apify_actors/planning_scraper/main.py +175 -0
  16. property_shared-0.1.0/apify_actors/planning_scraper/planning_scraper.py +579 -0
  17. property_shared-0.1.0/apify_actors/planning_scraper/requirements.txt +14 -0
  18. property_shared-0.1.0/app/__init__.py +1 -0
  19. property_shared-0.1.0/app/api/__init__.py +1 -0
  20. property_shared-0.1.0/app/api/routes.py +12 -0
  21. property_shared-0.1.0/app/api/v1/__init__.py +1 -0
  22. property_shared-0.1.0/app/api/v1/epc.py +90 -0
  23. property_shared-0.1.0/app/api/v1/health.py +8 -0
  24. property_shared-0.1.0/app/api/v1/meta.py +29 -0
  25. property_shared-0.1.0/app/api/v1/planning.py +249 -0
  26. property_shared-0.1.0/app/api/v1/ppd.py +178 -0
  27. property_shared-0.1.0/app/api/v1/report.py +66 -0
  28. property_shared-0.1.0/app/api/v1/rightmove.py +75 -0
  29. property_shared-0.1.0/app/clients/http.py +8 -0
  30. property_shared-0.1.0/app/core/__init__.py +1 -0
  31. property_shared-0.1.0/app/core/config.py +37 -0
  32. property_shared-0.1.0/app/core/logging.py +12 -0
  33. property_shared-0.1.0/app/main.py +40 -0
  34. property_shared-0.1.0/app/py.typed +0 -0
  35. property_shared-0.1.0/app/schemas/__init__.py +1 -0
  36. property_shared-0.1.0/app/schemas/epc.py +21 -0
  37. property_shared-0.1.0/app/schemas/ppd.py +41 -0
  38. property_shared-0.1.0/app/schemas/report.py +17 -0
  39. property_shared-0.1.0/app/schemas/rightmove.py +36 -0
  40. property_shared-0.1.0/app/services/__init__.py +1 -0
  41. property_shared-0.1.0/app/services/epc_service.py +52 -0
  42. property_shared-0.1.0/app/services/rightmove_service.py +95 -0
  43. property_shared-0.1.0/app/tasks/__init__.py +1 -0
  44. property_shared-0.1.0/app/templates/report.html +370 -0
  45. property_shared-0.1.0/app/utils/polite.py +34 -0
  46. property_shared-0.1.0/app/web/__init__.py +2 -0
  47. property_shared-0.1.0/app/web/routes.py +16 -0
  48. property_shared-0.1.0/app/web/templates/index.html +98 -0
  49. property_shared-0.1.0/fly.toml +40 -0
  50. property_shared-0.1.0/phase2_plan.md +92 -0
  51. property_shared-0.1.0/phase3_plan.md +239 -0
  52. property_shared-0.1.0/property_cli/__init__.py +2 -0
  53. property_shared-0.1.0/property_cli/main.py +945 -0
  54. property_shared-0.1.0/property_cli/py.typed +0 -0
  55. property_shared-0.1.0/property_core/__init__.py +31 -0
  56. property_shared-0.1.0/property_core/enrichment.py +143 -0
  57. property_shared-0.1.0/property_core/epc_client.py +266 -0
  58. property_shared-0.1.0/property_core/models/__init__.py +40 -0
  59. property_shared-0.1.0/property_core/models/epc.py +52 -0
  60. property_shared-0.1.0/property_core/models/ppd.py +95 -0
  61. property_shared-0.1.0/property_core/models/report.py +125 -0
  62. property_shared-0.1.0/property_core/models/rightmove.py +89 -0
  63. property_shared-0.1.0/property_core/planning_councils.json +1249 -0
  64. property_shared-0.1.0/property_core/planning_diagnostics.py +295 -0
  65. property_shared-0.1.0/property_core/planning_scraper.py +1134 -0
  66. property_shared-0.1.0/property_core/planning_service.py +279 -0
  67. property_shared-0.1.0/property_core/postcode_client.py +69 -0
  68. property_shared-0.1.0/property_core/ppd_client.py +545 -0
  69. property_shared-0.1.0/property_core/ppd_service.py +473 -0
  70. property_shared-0.1.0/property_core/py.typed +0 -0
  71. property_shared-0.1.0/property_core/rental_service.py +94 -0
  72. property_shared-0.1.0/property_core/report_service.py +523 -0
  73. property_shared-0.1.0/property_core/rightmove_location.py +124 -0
  74. property_shared-0.1.0/property_core/rightmove_scraper.py +537 -0
  75. property_shared-0.1.0/pyproject.toml +56 -0
  76. property_shared-0.1.0/scripts/update_councils_from_verification.py +134 -0
  77. property_shared-0.1.0/scripts/verify_councils.py +454 -0
  78. property_shared-0.1.0/tests/test_epc_service_live.py +41 -0
  79. property_shared-0.1.0/tests/test_ppd_form_search_live.py +31 -0
  80. property_shared-0.1.0/tests/test_ppd_service_live.py +47 -0
  81. property_shared-0.1.0/tests/test_rightmove_service_live.py +107 -0
  82. property_shared-0.1.0/uv.lock +1132 -0
@@ -0,0 +1,52 @@
1
+ # Git
2
+ .git
3
+ .gitignore
4
+
5
+ # Python
6
+ __pycache__
7
+ *.py[cod]
8
+ *$py.class
9
+ *.so
10
+ .Python
11
+ .venv
12
+ venv
13
+ ENV
14
+ env
15
+ .eggs
16
+ *.egg-info
17
+ *.egg
18
+
19
+ # Testing/Dev
20
+ .pytest_cache
21
+ .coverage
22
+ htmlcov
23
+ .tox
24
+ .mypy_cache
25
+ .ruff_cache
26
+
27
+ # IDE
28
+ .vscode
29
+ .idea
30
+ *.swp
31
+ *.swo
32
+
33
+ # Output/Logs
34
+ output/
35
+ *.log
36
+
37
+ # Apify (not needed for Fly deployment)
38
+ apify_actors/
39
+
40
+ # Docs/Tests (not needed at runtime)
41
+ tests/
42
+ docs/
43
+ *.md
44
+ !README.md
45
+
46
+ # Environment
47
+ .env
48
+ .env.*
49
+
50
+ # Misc
51
+ .DS_Store
52
+ Thumbs.db
@@ -0,0 +1,252 @@
1
+ # Virtual envs and tooling
2
+ .venv/
3
+ .env
4
+ .env.*
5
+ __pycache__/
6
+ *.pyc
7
+ *.pyo
8
+ *.pyd
9
+ .pytest_cache/
10
+ .mypy_cache/
11
+ .coverage
12
+ htmlcov/
13
+
14
+ # Editors
15
+ .vscode/
16
+ .idea/
17
+ .DS_Store
18
+
19
+ # Bytecode and build
20
+ build/
21
+ dist/
22
+ *.egg-info/
23
+
24
+ # Local data/cache
25
+ /data/
26
+ *.db
27
+
28
+ # Byte-compiled / optimized / DLL files
29
+ __pycache__/
30
+ *.py[codz]
31
+ *$py.class
32
+
33
+ # C extensions
34
+ *.so
35
+
36
+ # Distribution / packaging
37
+ .Python
38
+ build/
39
+ develop-eggs/
40
+ dist/
41
+ downloads/
42
+ eggs/
43
+ .eggs/
44
+ lib/
45
+ lib64/
46
+ parts/
47
+ sdist/
48
+ var/
49
+ wheels/
50
+ share/python-wheels/
51
+ *.egg-info/
52
+ .installed.cfg
53
+ *.egg
54
+ MANIFEST
55
+
56
+ # PyInstaller
57
+ # Usually these files are written by a python script from a template
58
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
59
+ *.manifest
60
+ *.spec
61
+
62
+ # Installer logs
63
+ pip-log.txt
64
+ pip-delete-this-directory.txt
65
+
66
+ # Unit test / coverage reports
67
+ htmlcov/
68
+ .tox/
69
+ .nox/
70
+ .coverage
71
+ .coverage.*
72
+ .cache
73
+ nosetests.xml
74
+ coverage.xml
75
+ *.cover
76
+ *.py.cover
77
+ .hypothesis/
78
+ .pytest_cache/
79
+ cover/
80
+
81
+ # Translations
82
+ *.mo
83
+ *.pot
84
+
85
+ # Django stuff:
86
+ *.log
87
+ local_settings.py
88
+ db.sqlite3
89
+ db.sqlite3-journal
90
+
91
+ # Flask stuff:
92
+ instance/
93
+ .webassets-cache
94
+
95
+ # Scrapy stuff:
96
+ .scrapy
97
+
98
+ # Sphinx documentation
99
+ docs/_build/
100
+
101
+ # PyBuilder
102
+ .pybuilder/
103
+ target/
104
+
105
+ # Jupyter Notebook
106
+ .ipynb_checkpoints
107
+
108
+ # IPython
109
+ profile_default/
110
+ ipython_config.py
111
+
112
+ # pyenv
113
+ # For a library or package, you might want to ignore these files since the code is
114
+ # intended to run in multiple environments; otherwise, check them in:
115
+ # .python-version
116
+
117
+ # pipenv
118
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
119
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
120
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
121
+ # install all needed dependencies.
122
+ # Pipfile.lock
123
+
124
+ # UV
125
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
126
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
127
+ # commonly ignored for libraries.
128
+ # uv.lock
129
+
130
+ # poetry
131
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
132
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
133
+ # commonly ignored for libraries.
134
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
135
+ # poetry.lock
136
+ # poetry.toml
137
+
138
+ # pdm
139
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
140
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
141
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
142
+ # pdm.lock
143
+ # pdm.toml
144
+ .pdm-python
145
+ .pdm-build/
146
+
147
+ # pixi
148
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
149
+ # pixi.lock
150
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
151
+ # in the .venv directory. It is recommended not to include this directory in version control.
152
+ .pixi
153
+
154
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
155
+ __pypackages__/
156
+
157
+ # Celery stuff
158
+ celerybeat-schedule
159
+ celerybeat.pid
160
+
161
+ # Redis
162
+ *.rdb
163
+ *.aof
164
+ *.pid
165
+
166
+ # RabbitMQ
167
+ mnesia/
168
+ rabbitmq/
169
+ rabbitmq-data/
170
+
171
+ # ActiveMQ
172
+ activemq-data/
173
+
174
+ # SageMath parsed files
175
+ *.sage.py
176
+
177
+ # Environments
178
+ .env
179
+ .envrc
180
+ .venv
181
+ env/
182
+ venv/
183
+ ENV/
184
+ env.bak/
185
+ venv.bak/
186
+
187
+ # Spyder project settings
188
+ .spyderproject
189
+ .spyproject
190
+
191
+ # Rope project settings
192
+ .ropeproject
193
+
194
+ # mkdocs documentation
195
+ /site
196
+
197
+ # mypy
198
+ .mypy_cache/
199
+ .dmypy.json
200
+ dmypy.json
201
+
202
+ # Pyre type checker
203
+ .pyre/
204
+
205
+ # pytype static type analyzer
206
+ .pytype/
207
+
208
+ # Cython debug symbols
209
+ cython_debug/
210
+
211
+ # PyCharm
212
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
213
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
214
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
215
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
216
+ # .idea/
217
+
218
+ # Abstra
219
+ # Abstra is an AI-powered process automation framework.
220
+ # Ignore directories containing user credentials, local state, and settings.
221
+ # Learn more at https://abstra.io/docs
222
+ .abstra/
223
+
224
+ # Visual Studio Code
225
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
226
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
227
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
228
+ # you could uncomment the following to ignore the entire vscode folder
229
+ # .vscode/
230
+
231
+ # Ruff stuff:
232
+ .ruff_cache/
233
+
234
+ # PyPI configuration file
235
+ .pypirc
236
+
237
+ # Marimo
238
+ marimo/_static/
239
+ marimo/_lsp/
240
+ __marimo__/
241
+
242
+ # Streamlit
243
+ .streamlit/secrets.toml
244
+
245
+ # Planning scraper test output
246
+ output/
247
+
248
+ # Local/temp files
249
+ AGENTS.md
250
+ skills/
251
+ verification_*.csv
252
+ example_ref/
@@ -0,0 +1,42 @@
1
+ stages:
2
+ - test
3
+ - publish
4
+
5
+ variables:
6
+ UV_CACHE_DIR: .uv-cache
7
+
8
+ .uv-setup: &uv-setup
9
+ before_script:
10
+ - curl -LsSf https://astral.sh/uv/install.sh | sh
11
+ - source $HOME/.local/bin/env
12
+ - uv sync --extra dev
13
+
14
+ cache:
15
+ paths:
16
+ - .uv-cache/
17
+
18
+ test:
19
+ stage: test
20
+ image: python:3.12
21
+ <<: *uv-setup
22
+ script:
23
+ - uv run pytest -v
24
+ rules:
25
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
26
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
27
+
28
+ publish:
29
+ stage: publish
30
+ image: python:3.12
31
+ environment: pypi
32
+ id_tokens:
33
+ PYPI_ID_TOKEN:
34
+ aud: pypi
35
+ before_script:
36
+ - curl -LsSf https://astral.sh/uv/install.sh | sh
37
+ - source $HOME/.local/bin/env
38
+ script:
39
+ - uv build
40
+ - uv publish --trusted-publishing always
41
+ rules:
42
+ - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
@@ -0,0 +1,124 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ Property Shared is a FastAPI service + pure-Python core library for UK property data. It integrates:
8
+ - **PPD** (Price Paid Data) - Land Registry transactions via SPARQL/Linked Data API
9
+ - **EPC** - Energy Performance Certificates (requires API credentials)
10
+ - **Rightmove** - Property listings via scraping with built-in politeness
11
+ - **Planning** - UK council planning applications via vision-guided browser automation (98 verified councils)
12
+
13
+ ## Commands
14
+
15
+ # Start .venv
16
+
17
+ ``bash
18
+ act
19
+ ```
20
+
21
+ ```bash
22
+ # Install dependencies (with dev extras)
23
+ uv sync --extra dev
24
+
25
+ # Run API server
26
+ uv run property-api # production mode
27
+ uv run uvicorn app.main:app --reload # dev mode with reload
28
+
29
+ # Run CLI (core mode - no server needed)
30
+ uv run --extra cli property-cli meta
31
+ uv run --extra cli property-cli ppd comps "SW1A 1AA" --months 24
32
+ uv run --extra cli property-cli rightmove search-url "SW1A 1AA"
33
+
34
+ # CLI targeting running API (add --api-url)
35
+ uv run --extra cli property-cli ppd comps "SW1A 1AA" --api-url http://localhost:8000
36
+
37
+ # Tests
38
+ uv run --extra dev pytest # unit tests (mocked)
39
+ RUN_LIVE_TESTS=1 uv run --extra dev pytest # live network tests
40
+
41
+ # Single test
42
+ uv run --extra dev pytest tests/test_ppd_service_live.py -v
43
+ ```
44
+
45
+ ## Architecture
46
+
47
+ ```
48
+ property_core/ # Pure Python library (no FastAPI, no DB assumptions)
49
+ ├── models/ # Domain Pydantic models
50
+ │ ├── ppd.py # PPDTransaction, PPDCompsResponse, SubjectProperty, etc.
51
+ │ ├── epc.py # EPCData
52
+ │ ├── rightmove.py # RightmoveListing, RightmoveListingDetail
53
+ │ └── report.py # PropertyReport, SaleHistory, MarketAnalysis, etc.
54
+ ├── ppd_client.py # Transport: Land Registry SPARQL + Linked Data API → dicts
55
+ ├── epc_client.py # Transport: EPC registry (async) → dicts/tuples
56
+ ├── rightmove_scraper.py # Transport: listings scraper (sync) → dataclasses
57
+ ├── rightmove_location.py # Transport: search URL builder (sync)
58
+ ├── postcode_client.py # Transport: postcodes.io → dicts
59
+ ├── ppd_service.py # Domain service: SPARQL parsing → typed PPD models (sync)
60
+ ├── planning_service.py # Domain service: council matching + URL building (sync)
61
+ ├── report_service.py # Product pipeline: multi-source aggregation (async)
62
+ ├── rental_service.py # Standalone rental analysis with yield calculation (async)
63
+ ├── enrichment.py # EPC enrichment pipeline + compute_enriched_stats()
64
+ ├── planning_scraper.py # Vision-guided planning portal scraper (Playwright + OpenAI)
65
+ └── planning_councils.json # Verified council database (98 councils, 6 system types)
66
+
67
+ app/ # FastAPI service (thin HTTP wrapper)
68
+ ├── api/v1/ # Versioned routers (import services from property_core)
69
+ ├── services/ # API-specific adapters (async threading, config binding)
70
+ │ ├── epc_service.py # Config-binding wrapper around EPCClient
71
+ │ └── rightmove_service.py # anyio.to_thread + PoliteLimiter
72
+ ├── schemas/ # API envelope models (import domain models from core)
73
+ ├── core/config.py # Settings via pydantic-settings (reads .env)
74
+ └── web/ # Demo UI at /demo
75
+
76
+ property_cli/ # Typer CLI (imports only from property_core)
77
+ └── main.py # All commands; --api-url switches to HTTP mode
78
+ ```
79
+
80
+ **Three-layer separation**:
81
+ - Transport clients (raw HTTP/SPARQL → dicts)
82
+ - Domain services (parsing + orchestration → typed Pydantic models)
83
+ - API layer (envelopes, async threading, rate limiting)
84
+
85
+ **Data flow**: API router → Core service (domain logic) → Core client (network)
86
+
87
+ ## Key Patterns
88
+
89
+ - **Dual-mode CLI**: Commands call `property_core` directly by default (fast, offline-capable). Add `--api-url` to route through the HTTP API instead.
90
+ - **Domain service guardrails**: `property_core/ppd_service.py` enforces limits (MAX_LIMIT=200, FORM_MAX_LIMIT=50) and normalizes responses. API routers are thin wrappers.
91
+ - **`include_raw` pattern**: All endpoints normalize data by default. Pass `include_raw=true` to get the original source data alongside normalized fields. EPC, PPD (transactions/address-search), Rightmove (listings), and Planning (council-for-postcode) all support this.
92
+ - **Area stats**: `PPDCompsResponse` includes `percentile_25`, `percentile_75` for price quartiles. When an address is provided and found, also includes `subject_price_percentile` (0-100) and `subject_vs_median_pct` (e.g., +10.8 means 10.8% above median).
93
+ - **EPC enrichment**: PPD comps can be enriched with EPC floor area via `enrich_epc=true` on the comps endpoint (or `--enrich-epc` in CLI). Groups comps by postcode, fetches all EPC certs per postcode (one API call each), fuzzy-matches addresses, and attaches derived fields (`epc_floor_area_sqm`, `price_per_sqft`, `epc_rating`, etc.) plus the full matched cert (`epc_match`) and confidence score (`epc_match_score`). After enrichment, call `compute_enriched_stats()` to populate `median_price_per_sqft` and `epc_match_rate`.
94
+ - **Standalone rental analysis**: `analyze_rentals(postcode, purchase_price=N)` returns rental market stats (median/average rent, listing count) with optional gross yield calculation. No full report needed.
95
+ - **Live test gating**: Tests making real network calls check `RUN_LIVE_TESTS=1` and skip gracefully on 503 or missing credentials.
96
+
97
+ ## Environment Variables
98
+
99
+ Copy `.env.example` to `.env`. Key variables:
100
+ - `EPC_API_EMAIL` / `EPC_API_KEY` - Required for EPC endpoints
101
+ - `RIGHTMOVE_DELAY_SECONDS` - Rate limit delay (default 0.6s)
102
+ - `OPENAI_API_KEY` - Required for planning scraper (vision extraction)
103
+ - `PLAYWRIGHT_PROXY_URL` - Optional residential proxy for planning scraper (councils block datacenter IPs)
104
+
105
+ ## Using as a Library
106
+
107
+ Install in another project: `pip install /path/to/property_shared` or add to dependencies.
108
+
109
+ ```python
110
+ # Domain services (typed models, no FastAPI needed)
111
+ from property_core import PPDService, PlanningService, PropertyReportService
112
+
113
+ # Transport clients (raw dicts)
114
+ from property_core import PricePaidDataClient, EPCClient, RightmoveLocationAPI, fetch_listings, PostcodeClient
115
+ from property_core import enrich_comps_with_epc, compute_enriched_stats, fetch_listing, analyze_rentals
116
+
117
+ # Domain models
118
+ from property_core.models.ppd import PPDTransaction, PPDCompsResponse
119
+ from property_core.models.epc import EPCData
120
+ from property_core.models.report import PropertyReport
121
+
122
+ # Planning scraper (requires playwright, openai)
123
+ from property_core.planning_scraper import scrape_planning_application, search_planning_by_postcode
124
+ ```
@@ -0,0 +1,33 @@
1
+ FROM python:3.11-slim
2
+
3
+ ENV PYTHONDONTWRITEBYTECODE=1 \
4
+ PYTHONUNBUFFERED=1 \
5
+ UV_PROJECT_ENVIRONMENT=/opt/venv \
6
+ PATH="/opt/venv/bin:$PATH"
7
+
8
+ WORKDIR /app
9
+
10
+ # System deps for Playwright/Chromium
11
+ RUN apt-get update && apt-get install -y --no-install-recommends \
12
+ libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 \
13
+ libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 \
14
+ libgbm1 libasound2 libpango-1.0-0 libcairo2 libatspi2.0-0 \
15
+ && rm -rf /var/lib/apt/lists/*
16
+
17
+ RUN pip install --no-cache-dir uv
18
+
19
+ # Copy dependency manifests first for better layer caching
20
+ COPY pyproject.toml uv.lock README.md ./
21
+ RUN uv sync --frozen --no-dev --extra api
22
+
23
+ # Install Playwright browsers (Chromium only to save space)
24
+ RUN playwright install chromium
25
+
26
+ # Copy application code
27
+ COPY app ./app
28
+ COPY property_core ./property_core
29
+
30
+ EXPOSE 8080
31
+
32
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]
33
+
@@ -0,0 +1,55 @@
1
+ # Development Guidelines
2
+
3
+ ## Architecture
4
+
5
+ Three-layer pattern:
6
+
7
+ 1. **Transport clients** (`property_core/*_client.py`)
8
+ - Raw HTTP calls → dicts
9
+ - Handle rate limiting, retries, auth
10
+ - Examples: `epc_client.py`, `ppd_client.py`, `postcode_client.py`
11
+
12
+ 2. **Domain services** (`property_core/*_service.py`)
13
+ - Parse raw data → typed Pydantic models
14
+ - Business logic, validation, orchestration
15
+ - Examples: `ppd_service.py`, `planning_service.py`
16
+
17
+ 3. **API routers** (`app/api/v1/*.py`)
18
+ - Thin HTTP wrappers over services
19
+ - Request validation, response envelopes
20
+
21
+ ## Adding a New Data Source
22
+
23
+ 1. Create transport client in `property_core/new_client.py`
24
+ 2. Create domain service in `property_core/new_service.py` (if needed)
25
+ 3. Add models to `property_core/models/new.py`
26
+ 4. Export from `property_core/models/__init__.py`
27
+ 5. Export from `property_core/__init__.py`
28
+ 6. Add API router in `app/api/v1/new.py` (optional)
29
+
30
+ ## Error Handling
31
+
32
+ - **Not found**: Return `None` (let caller decide)
33
+ - **Invalid input**: Raise `ValueError` with helpful message
34
+ - **Network errors**: Let bubble up or wrap in domain-specific exception
35
+ - **Debugging**: Use `include_raw=True` instead of logging
36
+
37
+ ## Testing
38
+
39
+ All tests are live integration tests:
40
+
41
+ ```bash
42
+ RUN_LIVE_TESTS=1 uv run --extra dev pytest -v
43
+ ```
44
+
45
+ Tests skip gracefully on 503/network errors.
46
+
47
+ Env vars: `RIGHTMOVE_TEST_POSTCODE`, `EPC_API_EMAIL`, `EPC_API_KEY`
48
+
49
+ ## Code Style
50
+
51
+ - Type hints on all functions
52
+ - Docstrings on public functions
53
+ - Private functions with `_` prefix
54
+ - Pydantic models with `Field()` for defaults
55
+ - `from __future__ import annotations` in all modules