pyfunda 2.9.0__tar.gz → 3.0.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.
- pyfunda-3.0.0/PKG-INFO +250 -0
- pyfunda-3.0.0/README.md +225 -0
- pyfunda-3.0.0/docs/API.md +211 -0
- pyfunda-3.0.0/docs/DEVELOPMENT.md +75 -0
- pyfunda-3.0.0/examples/almere_age_rank.py +264 -0
- pyfunda-3.0.0/examples/analysis.ipynb +158 -0
- pyfunda-3.0.0/examples/batch_details.py +40 -0
- pyfunda-3.0.0/examples/broker_due_diligence.py +54 -0
- pyfunda-3.0.0/examples/enrichment_export.py +54 -0
- pyfunda-3.0.0/examples/export_to_csv.py +127 -0
- pyfunda-3.0.0/examples/full_api_walkthrough.py +141 -0
- pyfunda-3.0.0/examples/neighborhood_market_snapshot.py +65 -0
- pyfunda-3.0.0/examples/new_listings_alert.py +116 -0
- pyfunda-3.0.0/examples/poll_new_listings.py +55 -0
- pyfunda-3.0.0/examples/price_history.py +55 -0
- pyfunda-3.0.0/examples/price_tracker.py +130 -0
- pyfunda-3.0.0/examples/search_sold.py +68 -0
- pyfunda-3.0.0/examples/similar_sales_comp.py +48 -0
- pyfunda-3.0.0/funda/__init__.py +83 -0
- pyfunda-3.0.0/funda/_detail_parser.py +390 -0
- pyfunda-3.0.0/funda/_enrichment_parser.py +338 -0
- pyfunda-3.0.0/funda/_parallel.py +83 -0
- pyfunda-3.0.0/funda/_parse_helpers.py +180 -0
- pyfunda-3.0.0/funda/_price_history_parser.py +47 -0
- pyfunda-3.0.0/funda/_search_parser.py +199 -0
- pyfunda-3.0.0/funda/_transport.py +225 -0
- pyfunda-3.0.0/funda/constants.py +88 -0
- pyfunda-3.0.0/funda/exceptions.py +25 -0
- pyfunda-3.0.0/funda/funda.py +459 -0
- pyfunda-3.0.0/funda/headers.py +49 -0
- pyfunda-3.0.0/funda/listing.py +374 -0
- pyfunda-3.0.0/funda/models.py +27 -0
- pyfunda-3.0.0/funda/parsing.py +29 -0
- pyfunda-3.0.0/funda/search.py +237 -0
- {pyfunda-2.9.0 → pyfunda-3.0.0}/pyproject.toml +2 -1
- pyfunda-3.0.0/tests/__init__.py +1 -0
- pyfunda-3.0.0/tests/test_client.py +110 -0
- pyfunda-3.0.0/tests/test_enrichment_parser.py +145 -0
- pyfunda-3.0.0/tests/test_live.py +52 -0
- pyfunda-3.0.0/tests/test_models.py +46 -0
- pyfunda-3.0.0/tests/test_search.py +59 -0
- pyfunda-3.0.0/tests/test_transport_parallel.py +122 -0
- pyfunda-2.9.0/PKG-INFO +0 -717
- pyfunda-2.9.0/README.md +0 -693
- pyfunda-2.9.0/examples/analysis.ipynb +0 -215
- pyfunda-2.9.0/examples/export_to_csv.py +0 -127
- pyfunda-2.9.0/examples/new_listings_alert.py +0 -117
- pyfunda-2.9.0/examples/poll_new_listings.py +0 -62
- pyfunda-2.9.0/examples/price_history.py +0 -79
- pyfunda-2.9.0/examples/price_tracker.py +0 -124
- pyfunda-2.9.0/examples/search_sold.py +0 -90
- pyfunda-2.9.0/funda/__init__.py +0 -22
- pyfunda-2.9.0/funda/funda.py +0 -1435
- pyfunda-2.9.0/funda/listing.py +0 -138
- pyfunda-2.9.0/test_all_flows.py +0 -1160
- {pyfunda-2.9.0 → pyfunda-3.0.0}/.dockerignore +0 -0
- {pyfunda-2.9.0 → pyfunda-3.0.0}/.github/FUNDING.yml +0 -0
- {pyfunda-2.9.0 → pyfunda-3.0.0}/.github/workflows/publish.yml +0 -0
- {pyfunda-2.9.0 → pyfunda-3.0.0}/.gitignore +0 -0
- {pyfunda-2.9.0 → pyfunda-3.0.0}/Dockerfile +0 -0
- {pyfunda-2.9.0 → pyfunda-3.0.0}/LICENSE +0 -0
- {pyfunda-2.9.0 → pyfunda-3.0.0}/funda/py.typed +0 -0
pyfunda-3.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyfunda
|
|
3
|
+
Version: 3.0.0
|
|
4
|
+
Summary: Python API for Funda.nl real estate listings
|
|
5
|
+
Project-URL: Homepage, https://github.com/0xMH/pyfunda
|
|
6
|
+
Project-URL: Repository, https://github.com/0xMH/pyfunda
|
|
7
|
+
Project-URL: Issues, https://github.com/0xMH/pyfunda/issues
|
|
8
|
+
Author: 0xMH
|
|
9
|
+
License-Expression: AGPL-3.0-or-later
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: api,funda,housing,netherlands,real-estate,scraper
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: curl-cffi>=0.14.0
|
|
22
|
+
Requires-Dist: tls-client>=1.0.1
|
|
23
|
+
Requires-Dist: typing-extensions>=4.0.0
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# pyfunda
|
|
27
|
+
|
|
28
|
+
Python client for Funda.nl real estate data.
|
|
29
|
+
|
|
30
|
+
pyfunda talks to Funda's app-facing JSON endpoints and returns typed Python
|
|
31
|
+
objects instead of browser-scraped HTML. It is designed for scripts, analysis,
|
|
32
|
+
alerts, exports, and lightweight enrichment workflows.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install pyfunda
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
For local development:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
uv sync
|
|
44
|
+
uv run python -m unittest discover -s tests
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Quick Start
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from funda import Funda
|
|
51
|
+
|
|
52
|
+
with Funda() as client:
|
|
53
|
+
listing = client.listing(43117443)
|
|
54
|
+
print(listing.title, listing.city, listing.price.amount)
|
|
55
|
+
|
|
56
|
+
results = client.search("amsterdam", max_price=500000)
|
|
57
|
+
for item in results:
|
|
58
|
+
print(item.title, item.price.amount, item.url)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Core API
|
|
62
|
+
|
|
63
|
+
### `listing(listing_id)`
|
|
64
|
+
|
|
65
|
+
Fetch a single listing by global id, tiny id, or Funda URL.
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
listing = client.listing(43117443)
|
|
69
|
+
listing = client.listing("https://www.funda.nl/detail/koop/amsterdam/house/43117443/")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### `listings(listing_ids, workers=8)`
|
|
73
|
+
|
|
74
|
+
Fetch multiple listings concurrently. Order is preserved.
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
details = client.listings([43117443, 43333315], workers=4)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### `search(location=None, **filters)`
|
|
81
|
+
|
|
82
|
+
Fetch one search page.
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
results = client.search(
|
|
86
|
+
"amsterdam",
|
|
87
|
+
category="buy", # buy, rent, sold
|
|
88
|
+
min_price=200000,
|
|
89
|
+
max_price=500000,
|
|
90
|
+
min_area=50,
|
|
91
|
+
min_bedrooms=2,
|
|
92
|
+
object_type="apartment",
|
|
93
|
+
sort="newest",
|
|
94
|
+
)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### `iter_search(location=None, max_pages=None, workers=1, **filters)`
|
|
98
|
+
|
|
99
|
+
Iterate across search pages. Set `workers > 1` with `max_pages` for concurrent
|
|
100
|
+
page fetching.
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
for listing in client.iter_search("utrecht", max_pages=5, workers=3):
|
|
104
|
+
print(listing.title)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Search Filters
|
|
108
|
+
|
|
109
|
+
Supported filters:
|
|
110
|
+
|
|
111
|
+
| Filter | Description |
|
|
112
|
+
| --- | --- |
|
|
113
|
+
| `category` | `buy`, `rent`, or `sold` |
|
|
114
|
+
| `status` | Availability values such as `available` or `negotiations` |
|
|
115
|
+
| `min_price`, `max_price` | Sale or rent price bounds |
|
|
116
|
+
| `min_area`, `max_area` | Living area bounds |
|
|
117
|
+
| `min_plot`, `max_plot` | Plot area bounds |
|
|
118
|
+
| `min_rooms`, `max_rooms` | Total room bounds |
|
|
119
|
+
| `min_bedrooms`, `max_bedrooms` | Bedroom bounds |
|
|
120
|
+
| `object_type` | `house`, `apartment`, or a sequence |
|
|
121
|
+
| `energy_label` | Energy label or sequence |
|
|
122
|
+
| `construction_type` | Construction type |
|
|
123
|
+
| `min_construction_year`, `max_construction_year` | Construction year bounds |
|
|
124
|
+
| `radius_km` | Radius search around exactly one location |
|
|
125
|
+
| `sort` | `newest`, `oldest`, `price_asc`, `price_desc`, `area_asc`, `area_desc`, `plot_desc`, `city`, `postcode` |
|
|
126
|
+
| `page` | Page number for `search`; use `start_page` for `iter_search` |
|
|
127
|
+
|
|
128
|
+
## Listing Objects
|
|
129
|
+
|
|
130
|
+
`Listing` is an immutable dataclass with nested value objects.
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
listing.title
|
|
134
|
+
listing.city
|
|
135
|
+
listing.postcode
|
|
136
|
+
listing.price.amount
|
|
137
|
+
listing.price.formatted
|
|
138
|
+
listing.living_area
|
|
139
|
+
listing.plot_area
|
|
140
|
+
listing.bedrooms
|
|
141
|
+
listing.rooms_count
|
|
142
|
+
listing.energy_label
|
|
143
|
+
listing.status
|
|
144
|
+
listing.location.coordinates
|
|
145
|
+
listing.media.photo_urls
|
|
146
|
+
listing.broker
|
|
147
|
+
listing.sales_history
|
|
148
|
+
listing.raw
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Use `to_dict()` when you need serialization:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
data = listing.to_dict()
|
|
155
|
+
raw_data = listing.to_dict(include_raw=True)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Enrichment API
|
|
159
|
+
|
|
160
|
+
These methods are lazy and only make extra requests when called.
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
contact = client.contact_info(listing)
|
|
164
|
+
form = client.contact_form(listing)
|
|
165
|
+
summary = client.listing_summary(listing)
|
|
166
|
+
similar = client.similar_listings(listing)
|
|
167
|
+
insights = client.market_insights(listing)
|
|
168
|
+
broker = client.broker_info(listing)
|
|
169
|
+
broker_listings = client.broker_listings(listing)
|
|
170
|
+
reviews = client.broker_reviews(listing)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Price History
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
history = client.price_history(listing)
|
|
177
|
+
for change in history.changes:
|
|
178
|
+
print(change.date, change.human_price, change.status)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## New Listing Polling
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
latest_id = client.latest_listing_id()
|
|
185
|
+
|
|
186
|
+
for listing in client.new_listings(since_id=latest_id):
|
|
187
|
+
print(listing.title, listing.url)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Performance
|
|
191
|
+
|
|
192
|
+
Single requests are intentionally sequential:
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
client.listing(43117443)
|
|
196
|
+
client.search("amsterdam")
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Batch workflows can use parallel fetching:
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
client.listings(ids, workers=4)
|
|
203
|
+
list(client.iter_search("amsterdam", max_pages=4, workers=4))
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The client keeps a reusable per-thread worker pool for repeated batch calls and
|
|
207
|
+
caches the last working TLS fingerprint internally.
|
|
208
|
+
|
|
209
|
+
## Examples
|
|
210
|
+
|
|
211
|
+
Runnable examples live in `examples/`:
|
|
212
|
+
|
|
213
|
+
| File | Purpose |
|
|
214
|
+
| --- | --- |
|
|
215
|
+
| `full_api_walkthrough.py` | Small end-to-end walkthrough of the public API |
|
|
216
|
+
| `batch_details.py` | Parallel detail fetching for known ids |
|
|
217
|
+
| `broker_due_diligence.py` | Broker profile, reviews, and handled listings |
|
|
218
|
+
| `enrichment_export.py` | Export a listing plus enrichment data to JSON |
|
|
219
|
+
| `neighborhood_market_snapshot.py` | Compare search sample with local market insights |
|
|
220
|
+
| `similar_sales_comp.py` | Build comparable-sales rows from similar sold listings |
|
|
221
|
+
| `search_sold.py` | Search sold listings and print summary stats |
|
|
222
|
+
| `export_to_csv.py` | Export search results to CSV or XLSX |
|
|
223
|
+
| `new_listings_alert.py` | Alert on new listings matching a search |
|
|
224
|
+
| `poll_new_listings.py` | Poll by incrementing listing ids |
|
|
225
|
+
| `price_history.py` | Print historical price changes |
|
|
226
|
+
| `price_tracker.py` | Persist and track listing price changes |
|
|
227
|
+
| `almere_age_rank.py` | Compare construction year distribution |
|
|
228
|
+
| `analysis.ipynb` | Pandas analysis notebook |
|
|
229
|
+
|
|
230
|
+
## Tests
|
|
231
|
+
|
|
232
|
+
Fast local tests:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
uv run python -m unittest discover -s tests
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Live Funda smoke tests:
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
PYFUNDA_LIVE=1 uv run python -m unittest tests.test_live -v
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Live tests intentionally stay small: they verify listing, search, parallel
|
|
245
|
+
fetching, and enrichment endpoints without sweeping large result sets.
|
|
246
|
+
|
|
247
|
+
## More Documentation
|
|
248
|
+
|
|
249
|
+
- [API reference](docs/API.md)
|
|
250
|
+
- [Development and testing](docs/DEVELOPMENT.md)
|
pyfunda-3.0.0/README.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# pyfunda
|
|
2
|
+
|
|
3
|
+
Python client for Funda.nl real estate data.
|
|
4
|
+
|
|
5
|
+
pyfunda talks to Funda's app-facing JSON endpoints and returns typed Python
|
|
6
|
+
objects instead of browser-scraped HTML. It is designed for scripts, analysis,
|
|
7
|
+
alerts, exports, and lightweight enrichment workflows.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install pyfunda
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
For local development:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
uv sync
|
|
19
|
+
uv run python -m unittest discover -s tests
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from funda import Funda
|
|
26
|
+
|
|
27
|
+
with Funda() as client:
|
|
28
|
+
listing = client.listing(43117443)
|
|
29
|
+
print(listing.title, listing.city, listing.price.amount)
|
|
30
|
+
|
|
31
|
+
results = client.search("amsterdam", max_price=500000)
|
|
32
|
+
for item in results:
|
|
33
|
+
print(item.title, item.price.amount, item.url)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Core API
|
|
37
|
+
|
|
38
|
+
### `listing(listing_id)`
|
|
39
|
+
|
|
40
|
+
Fetch a single listing by global id, tiny id, or Funda URL.
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
listing = client.listing(43117443)
|
|
44
|
+
listing = client.listing("https://www.funda.nl/detail/koop/amsterdam/house/43117443/")
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### `listings(listing_ids, workers=8)`
|
|
48
|
+
|
|
49
|
+
Fetch multiple listings concurrently. Order is preserved.
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
details = client.listings([43117443, 43333315], workers=4)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### `search(location=None, **filters)`
|
|
56
|
+
|
|
57
|
+
Fetch one search page.
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
results = client.search(
|
|
61
|
+
"amsterdam",
|
|
62
|
+
category="buy", # buy, rent, sold
|
|
63
|
+
min_price=200000,
|
|
64
|
+
max_price=500000,
|
|
65
|
+
min_area=50,
|
|
66
|
+
min_bedrooms=2,
|
|
67
|
+
object_type="apartment",
|
|
68
|
+
sort="newest",
|
|
69
|
+
)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### `iter_search(location=None, max_pages=None, workers=1, **filters)`
|
|
73
|
+
|
|
74
|
+
Iterate across search pages. Set `workers > 1` with `max_pages` for concurrent
|
|
75
|
+
page fetching.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
for listing in client.iter_search("utrecht", max_pages=5, workers=3):
|
|
79
|
+
print(listing.title)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Search Filters
|
|
83
|
+
|
|
84
|
+
Supported filters:
|
|
85
|
+
|
|
86
|
+
| Filter | Description |
|
|
87
|
+
| --- | --- |
|
|
88
|
+
| `category` | `buy`, `rent`, or `sold` |
|
|
89
|
+
| `status` | Availability values such as `available` or `negotiations` |
|
|
90
|
+
| `min_price`, `max_price` | Sale or rent price bounds |
|
|
91
|
+
| `min_area`, `max_area` | Living area bounds |
|
|
92
|
+
| `min_plot`, `max_plot` | Plot area bounds |
|
|
93
|
+
| `min_rooms`, `max_rooms` | Total room bounds |
|
|
94
|
+
| `min_bedrooms`, `max_bedrooms` | Bedroom bounds |
|
|
95
|
+
| `object_type` | `house`, `apartment`, or a sequence |
|
|
96
|
+
| `energy_label` | Energy label or sequence |
|
|
97
|
+
| `construction_type` | Construction type |
|
|
98
|
+
| `min_construction_year`, `max_construction_year` | Construction year bounds |
|
|
99
|
+
| `radius_km` | Radius search around exactly one location |
|
|
100
|
+
| `sort` | `newest`, `oldest`, `price_asc`, `price_desc`, `area_asc`, `area_desc`, `plot_desc`, `city`, `postcode` |
|
|
101
|
+
| `page` | Page number for `search`; use `start_page` for `iter_search` |
|
|
102
|
+
|
|
103
|
+
## Listing Objects
|
|
104
|
+
|
|
105
|
+
`Listing` is an immutable dataclass with nested value objects.
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
listing.title
|
|
109
|
+
listing.city
|
|
110
|
+
listing.postcode
|
|
111
|
+
listing.price.amount
|
|
112
|
+
listing.price.formatted
|
|
113
|
+
listing.living_area
|
|
114
|
+
listing.plot_area
|
|
115
|
+
listing.bedrooms
|
|
116
|
+
listing.rooms_count
|
|
117
|
+
listing.energy_label
|
|
118
|
+
listing.status
|
|
119
|
+
listing.location.coordinates
|
|
120
|
+
listing.media.photo_urls
|
|
121
|
+
listing.broker
|
|
122
|
+
listing.sales_history
|
|
123
|
+
listing.raw
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Use `to_dict()` when you need serialization:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
data = listing.to_dict()
|
|
130
|
+
raw_data = listing.to_dict(include_raw=True)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Enrichment API
|
|
134
|
+
|
|
135
|
+
These methods are lazy and only make extra requests when called.
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
contact = client.contact_info(listing)
|
|
139
|
+
form = client.contact_form(listing)
|
|
140
|
+
summary = client.listing_summary(listing)
|
|
141
|
+
similar = client.similar_listings(listing)
|
|
142
|
+
insights = client.market_insights(listing)
|
|
143
|
+
broker = client.broker_info(listing)
|
|
144
|
+
broker_listings = client.broker_listings(listing)
|
|
145
|
+
reviews = client.broker_reviews(listing)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Price History
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
history = client.price_history(listing)
|
|
152
|
+
for change in history.changes:
|
|
153
|
+
print(change.date, change.human_price, change.status)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## New Listing Polling
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
latest_id = client.latest_listing_id()
|
|
160
|
+
|
|
161
|
+
for listing in client.new_listings(since_id=latest_id):
|
|
162
|
+
print(listing.title, listing.url)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Performance
|
|
166
|
+
|
|
167
|
+
Single requests are intentionally sequential:
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
client.listing(43117443)
|
|
171
|
+
client.search("amsterdam")
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Batch workflows can use parallel fetching:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
client.listings(ids, workers=4)
|
|
178
|
+
list(client.iter_search("amsterdam", max_pages=4, workers=4))
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The client keeps a reusable per-thread worker pool for repeated batch calls and
|
|
182
|
+
caches the last working TLS fingerprint internally.
|
|
183
|
+
|
|
184
|
+
## Examples
|
|
185
|
+
|
|
186
|
+
Runnable examples live in `examples/`:
|
|
187
|
+
|
|
188
|
+
| File | Purpose |
|
|
189
|
+
| --- | --- |
|
|
190
|
+
| `full_api_walkthrough.py` | Small end-to-end walkthrough of the public API |
|
|
191
|
+
| `batch_details.py` | Parallel detail fetching for known ids |
|
|
192
|
+
| `broker_due_diligence.py` | Broker profile, reviews, and handled listings |
|
|
193
|
+
| `enrichment_export.py` | Export a listing plus enrichment data to JSON |
|
|
194
|
+
| `neighborhood_market_snapshot.py` | Compare search sample with local market insights |
|
|
195
|
+
| `similar_sales_comp.py` | Build comparable-sales rows from similar sold listings |
|
|
196
|
+
| `search_sold.py` | Search sold listings and print summary stats |
|
|
197
|
+
| `export_to_csv.py` | Export search results to CSV or XLSX |
|
|
198
|
+
| `new_listings_alert.py` | Alert on new listings matching a search |
|
|
199
|
+
| `poll_new_listings.py` | Poll by incrementing listing ids |
|
|
200
|
+
| `price_history.py` | Print historical price changes |
|
|
201
|
+
| `price_tracker.py` | Persist and track listing price changes |
|
|
202
|
+
| `almere_age_rank.py` | Compare construction year distribution |
|
|
203
|
+
| `analysis.ipynb` | Pandas analysis notebook |
|
|
204
|
+
|
|
205
|
+
## Tests
|
|
206
|
+
|
|
207
|
+
Fast local tests:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
uv run python -m unittest discover -s tests
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Live Funda smoke tests:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
PYFUNDA_LIVE=1 uv run python -m unittest tests.test_live -v
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Live tests intentionally stay small: they verify listing, search, parallel
|
|
220
|
+
fetching, and enrichment endpoints without sweeping large result sets.
|
|
221
|
+
|
|
222
|
+
## More Documentation
|
|
223
|
+
|
|
224
|
+
- [API reference](docs/API.md)
|
|
225
|
+
- [Development and testing](docs/DEVELOPMENT.md)
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# API Reference
|
|
2
|
+
|
|
3
|
+
This is the public pyfunda API as of the current redesign. Private modules and
|
|
4
|
+
underscore-prefixed classes are implementation details.
|
|
5
|
+
|
|
6
|
+
## Client
|
|
7
|
+
|
|
8
|
+
```python
|
|
9
|
+
from funda import Funda
|
|
10
|
+
|
|
11
|
+
client = Funda(timeout=30, max_retries=5, retry_backoff=0.1)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Use the client as a context manager when possible:
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
with Funda() as client:
|
|
18
|
+
listing = client.listing(43117443)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### `Funda.listing(listing_id)`
|
|
22
|
+
|
|
23
|
+
Returns a `Listing`.
|
|
24
|
+
|
|
25
|
+
`listing_id` may be:
|
|
26
|
+
|
|
27
|
+
- a global id
|
|
28
|
+
- a tiny id
|
|
29
|
+
- a Funda detail URL
|
|
30
|
+
- an older Funda slug URL containing a 7-9 digit id
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
|
|
34
|
+
- `ListingNotFound` for `404`
|
|
35
|
+
- `FundaRequestError` for transport rejection or unexpected HTTP status
|
|
36
|
+
- `ValueError` for invalid ids or URLs
|
|
37
|
+
|
|
38
|
+
### `Funda.listings(listing_ids, workers=8)`
|
|
39
|
+
|
|
40
|
+
Fetches details for many listing ids concurrently and returns `list[Listing]`.
|
|
41
|
+
The output order matches the input order.
|
|
42
|
+
|
|
43
|
+
Use this for batches. For one listing, use `listing()`.
|
|
44
|
+
|
|
45
|
+
### `Funda.search(location=None, **filters)`
|
|
46
|
+
|
|
47
|
+
Fetches one search page and returns `list[Listing]`.
|
|
48
|
+
|
|
49
|
+
Common filters:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
client.search(
|
|
53
|
+
"amsterdam",
|
|
54
|
+
category="buy",
|
|
55
|
+
min_price=200000,
|
|
56
|
+
max_price=500000,
|
|
57
|
+
min_area=50,
|
|
58
|
+
max_area=120,
|
|
59
|
+
min_plot=100,
|
|
60
|
+
max_plot=500,
|
|
61
|
+
min_rooms=3,
|
|
62
|
+
max_rooms=6,
|
|
63
|
+
min_bedrooms=2,
|
|
64
|
+
max_bedrooms=4,
|
|
65
|
+
object_type=["house", "apartment"],
|
|
66
|
+
energy_label=["A", "A+"],
|
|
67
|
+
construction_type="existing",
|
|
68
|
+
min_construction_year=1990,
|
|
69
|
+
max_construction_year=2020,
|
|
70
|
+
radius_km=10,
|
|
71
|
+
sort="newest",
|
|
72
|
+
page=0,
|
|
73
|
+
)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Valid categories are `buy`, `rent`, and `sold`.
|
|
77
|
+
|
|
78
|
+
### `Funda.iter_search(location=None, start_page=0, max_pages=None, workers=1, **filters)`
|
|
79
|
+
|
|
80
|
+
Iterates search pages until an empty or short page is returned, or until
|
|
81
|
+
`max_pages` is reached.
|
|
82
|
+
|
|
83
|
+
Parallel page fetching requires `max_pages`:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
list(client.iter_search("amsterdam", max_pages=4, workers=4))
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### `Funda.latest_listing_id()`
|
|
90
|
+
|
|
91
|
+
Returns the highest listing global id visible in the search index.
|
|
92
|
+
|
|
93
|
+
### `Funda.new_listings(since_id, max_consecutive_404s=20)`
|
|
94
|
+
|
|
95
|
+
Yields details for newly discoverable global ids after `since_id`.
|
|
96
|
+
|
|
97
|
+
### `Funda.price_history(listing)`
|
|
98
|
+
|
|
99
|
+
Returns `PriceHistory` for a `Listing` or Funda URL.
|
|
100
|
+
|
|
101
|
+
### Enrichment Methods
|
|
102
|
+
|
|
103
|
+
These methods return extra data from auxiliary Funda endpoints:
|
|
104
|
+
|
|
105
|
+
| Method | Return |
|
|
106
|
+
| --- | --- |
|
|
107
|
+
| `contact_info(listing)` | `dict` with primary broker/contact fields |
|
|
108
|
+
| `contact_form(listing)` | `dict` with contact form availability |
|
|
109
|
+
| `listing_summary(listing)` | lightweight `Listing` |
|
|
110
|
+
| `similar_listings(listing)` | `dict` with recently listed/sold global ids |
|
|
111
|
+
| `market_insights(city, neighbourhood=None)` | `dict` with local market fields |
|
|
112
|
+
| `broker_info(broker)` | `dict` with broker profile fields |
|
|
113
|
+
| `broker_listings(broker)` | `list[dict]` of broker listings |
|
|
114
|
+
| `broker_reviews(broker)` | `dict` with aggregate and recent reviews |
|
|
115
|
+
|
|
116
|
+
`listing` arguments accept a `Listing`, a global id, a tiny id, or a Funda URL.
|
|
117
|
+
`broker` arguments accept a `Listing` with broker data or a broker id.
|
|
118
|
+
|
|
119
|
+
## Models
|
|
120
|
+
|
|
121
|
+
### `Listing`
|
|
122
|
+
|
|
123
|
+
`Listing` is frozen and slot-based. Prefer attributes over dict indexing.
|
|
124
|
+
|
|
125
|
+
Important fields:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
listing.id
|
|
129
|
+
listing.global_id
|
|
130
|
+
listing.tiny_id
|
|
131
|
+
listing.source
|
|
132
|
+
listing.offering_type
|
|
133
|
+
listing.address
|
|
134
|
+
listing.price
|
|
135
|
+
listing.areas
|
|
136
|
+
listing.rooms
|
|
137
|
+
listing.property_details
|
|
138
|
+
listing.location
|
|
139
|
+
listing.urls
|
|
140
|
+
listing.media
|
|
141
|
+
listing.brokers
|
|
142
|
+
listing.labels
|
|
143
|
+
listing.description
|
|
144
|
+
listing.characteristics
|
|
145
|
+
listing.sales_history
|
|
146
|
+
listing.parent_project
|
|
147
|
+
listing.insights
|
|
148
|
+
listing.raw
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Convenience properties:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
listing.title
|
|
155
|
+
listing.city
|
|
156
|
+
listing.postcode
|
|
157
|
+
listing.url
|
|
158
|
+
listing.detail_url
|
|
159
|
+
listing.broker
|
|
160
|
+
listing.living_area
|
|
161
|
+
listing.plot_area
|
|
162
|
+
listing.rooms_count
|
|
163
|
+
listing.bedrooms
|
|
164
|
+
listing.energy_label
|
|
165
|
+
listing.status
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Use:
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
listing.characteristic("Bouwjaar")
|
|
172
|
+
listing.to_dict()
|
|
173
|
+
listing.to_dict(include_raw=True)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Nested Value Objects
|
|
177
|
+
|
|
178
|
+
The main nested dataclasses are:
|
|
179
|
+
|
|
180
|
+
- `Address`
|
|
181
|
+
- `Price`
|
|
182
|
+
- `Areas`
|
|
183
|
+
- `Rooms`
|
|
184
|
+
- `PropertyDetails`
|
|
185
|
+
- `GeoLocation`
|
|
186
|
+
- `Urls`
|
|
187
|
+
- `Media`
|
|
188
|
+
- `MediaItem`
|
|
189
|
+
- `Broker`
|
|
190
|
+
- `SalesHistory`
|
|
191
|
+
- `Project`
|
|
192
|
+
- `Insights`
|
|
193
|
+
- `PriceHistory`
|
|
194
|
+
- `PriceChange`
|
|
195
|
+
|
|
196
|
+
Each supports `to_dict(include_raw=False)`.
|
|
197
|
+
|
|
198
|
+
## Exceptions
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
from funda import (
|
|
202
|
+
FundaError,
|
|
203
|
+
FundaRequestError,
|
|
204
|
+
FingerprintError,
|
|
205
|
+
ListingNotFound,
|
|
206
|
+
PriceHistoryError,
|
|
207
|
+
SearchError,
|
|
208
|
+
)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Use `FundaError` to catch any pyfunda-specific error.
|