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.
- property_shared-0.1.0/.dockerignore +52 -0
- property_shared-0.1.0/.gitignore +252 -0
- property_shared-0.1.0/.gitlab-ci.yml +42 -0
- property_shared-0.1.0/CLAUDE.md +124 -0
- property_shared-0.1.0/Dockerfile +33 -0
- property_shared-0.1.0/GUIDELINES.md +55 -0
- property_shared-0.1.0/PKG-INFO +136 -0
- property_shared-0.1.0/PLAN.md +102 -0
- property_shared-0.1.0/README.md +107 -0
- property_shared-0.1.0/USER_GUIDE.md +331 -0
- property_shared-0.1.0/apify_actors/planning_scraper/.actor/actor.json +16 -0
- property_shared-0.1.0/apify_actors/planning_scraper/Dockerfile +14 -0
- property_shared-0.1.0/apify_actors/planning_scraper/README.md +118 -0
- property_shared-0.1.0/apify_actors/planning_scraper/input_schema.json +73 -0
- property_shared-0.1.0/apify_actors/planning_scraper/main.py +175 -0
- property_shared-0.1.0/apify_actors/planning_scraper/planning_scraper.py +579 -0
- property_shared-0.1.0/apify_actors/planning_scraper/requirements.txt +14 -0
- property_shared-0.1.0/app/__init__.py +1 -0
- property_shared-0.1.0/app/api/__init__.py +1 -0
- property_shared-0.1.0/app/api/routes.py +12 -0
- property_shared-0.1.0/app/api/v1/__init__.py +1 -0
- property_shared-0.1.0/app/api/v1/epc.py +90 -0
- property_shared-0.1.0/app/api/v1/health.py +8 -0
- property_shared-0.1.0/app/api/v1/meta.py +29 -0
- property_shared-0.1.0/app/api/v1/planning.py +249 -0
- property_shared-0.1.0/app/api/v1/ppd.py +178 -0
- property_shared-0.1.0/app/api/v1/report.py +66 -0
- property_shared-0.1.0/app/api/v1/rightmove.py +75 -0
- property_shared-0.1.0/app/clients/http.py +8 -0
- property_shared-0.1.0/app/core/__init__.py +1 -0
- property_shared-0.1.0/app/core/config.py +37 -0
- property_shared-0.1.0/app/core/logging.py +12 -0
- property_shared-0.1.0/app/main.py +40 -0
- property_shared-0.1.0/app/py.typed +0 -0
- property_shared-0.1.0/app/schemas/__init__.py +1 -0
- property_shared-0.1.0/app/schemas/epc.py +21 -0
- property_shared-0.1.0/app/schemas/ppd.py +41 -0
- property_shared-0.1.0/app/schemas/report.py +17 -0
- property_shared-0.1.0/app/schemas/rightmove.py +36 -0
- property_shared-0.1.0/app/services/__init__.py +1 -0
- property_shared-0.1.0/app/services/epc_service.py +52 -0
- property_shared-0.1.0/app/services/rightmove_service.py +95 -0
- property_shared-0.1.0/app/tasks/__init__.py +1 -0
- property_shared-0.1.0/app/templates/report.html +370 -0
- property_shared-0.1.0/app/utils/polite.py +34 -0
- property_shared-0.1.0/app/web/__init__.py +2 -0
- property_shared-0.1.0/app/web/routes.py +16 -0
- property_shared-0.1.0/app/web/templates/index.html +98 -0
- property_shared-0.1.0/fly.toml +40 -0
- property_shared-0.1.0/phase2_plan.md +92 -0
- property_shared-0.1.0/phase3_plan.md +239 -0
- property_shared-0.1.0/property_cli/__init__.py +2 -0
- property_shared-0.1.0/property_cli/main.py +945 -0
- property_shared-0.1.0/property_cli/py.typed +0 -0
- property_shared-0.1.0/property_core/__init__.py +31 -0
- property_shared-0.1.0/property_core/enrichment.py +143 -0
- property_shared-0.1.0/property_core/epc_client.py +266 -0
- property_shared-0.1.0/property_core/models/__init__.py +40 -0
- property_shared-0.1.0/property_core/models/epc.py +52 -0
- property_shared-0.1.0/property_core/models/ppd.py +95 -0
- property_shared-0.1.0/property_core/models/report.py +125 -0
- property_shared-0.1.0/property_core/models/rightmove.py +89 -0
- property_shared-0.1.0/property_core/planning_councils.json +1249 -0
- property_shared-0.1.0/property_core/planning_diagnostics.py +295 -0
- property_shared-0.1.0/property_core/planning_scraper.py +1134 -0
- property_shared-0.1.0/property_core/planning_service.py +279 -0
- property_shared-0.1.0/property_core/postcode_client.py +69 -0
- property_shared-0.1.0/property_core/ppd_client.py +545 -0
- property_shared-0.1.0/property_core/ppd_service.py +473 -0
- property_shared-0.1.0/property_core/py.typed +0 -0
- property_shared-0.1.0/property_core/rental_service.py +94 -0
- property_shared-0.1.0/property_core/report_service.py +523 -0
- property_shared-0.1.0/property_core/rightmove_location.py +124 -0
- property_shared-0.1.0/property_core/rightmove_scraper.py +537 -0
- property_shared-0.1.0/pyproject.toml +56 -0
- property_shared-0.1.0/scripts/update_councils_from_verification.py +134 -0
- property_shared-0.1.0/scripts/verify_councils.py +454 -0
- property_shared-0.1.0/tests/test_epc_service_live.py +41 -0
- property_shared-0.1.0/tests/test_ppd_form_search_live.py +31 -0
- property_shared-0.1.0/tests/test_ppd_service_live.py +47 -0
- property_shared-0.1.0/tests/test_rightmove_service_live.py +107 -0
- 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
|