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.
Files changed (62) hide show
  1. pyfunda-3.0.0/PKG-INFO +250 -0
  2. pyfunda-3.0.0/README.md +225 -0
  3. pyfunda-3.0.0/docs/API.md +211 -0
  4. pyfunda-3.0.0/docs/DEVELOPMENT.md +75 -0
  5. pyfunda-3.0.0/examples/almere_age_rank.py +264 -0
  6. pyfunda-3.0.0/examples/analysis.ipynb +158 -0
  7. pyfunda-3.0.0/examples/batch_details.py +40 -0
  8. pyfunda-3.0.0/examples/broker_due_diligence.py +54 -0
  9. pyfunda-3.0.0/examples/enrichment_export.py +54 -0
  10. pyfunda-3.0.0/examples/export_to_csv.py +127 -0
  11. pyfunda-3.0.0/examples/full_api_walkthrough.py +141 -0
  12. pyfunda-3.0.0/examples/neighborhood_market_snapshot.py +65 -0
  13. pyfunda-3.0.0/examples/new_listings_alert.py +116 -0
  14. pyfunda-3.0.0/examples/poll_new_listings.py +55 -0
  15. pyfunda-3.0.0/examples/price_history.py +55 -0
  16. pyfunda-3.0.0/examples/price_tracker.py +130 -0
  17. pyfunda-3.0.0/examples/search_sold.py +68 -0
  18. pyfunda-3.0.0/examples/similar_sales_comp.py +48 -0
  19. pyfunda-3.0.0/funda/__init__.py +83 -0
  20. pyfunda-3.0.0/funda/_detail_parser.py +390 -0
  21. pyfunda-3.0.0/funda/_enrichment_parser.py +338 -0
  22. pyfunda-3.0.0/funda/_parallel.py +83 -0
  23. pyfunda-3.0.0/funda/_parse_helpers.py +180 -0
  24. pyfunda-3.0.0/funda/_price_history_parser.py +47 -0
  25. pyfunda-3.0.0/funda/_search_parser.py +199 -0
  26. pyfunda-3.0.0/funda/_transport.py +225 -0
  27. pyfunda-3.0.0/funda/constants.py +88 -0
  28. pyfunda-3.0.0/funda/exceptions.py +25 -0
  29. pyfunda-3.0.0/funda/funda.py +459 -0
  30. pyfunda-3.0.0/funda/headers.py +49 -0
  31. pyfunda-3.0.0/funda/listing.py +374 -0
  32. pyfunda-3.0.0/funda/models.py +27 -0
  33. pyfunda-3.0.0/funda/parsing.py +29 -0
  34. pyfunda-3.0.0/funda/search.py +237 -0
  35. {pyfunda-2.9.0 → pyfunda-3.0.0}/pyproject.toml +2 -1
  36. pyfunda-3.0.0/tests/__init__.py +1 -0
  37. pyfunda-3.0.0/tests/test_client.py +110 -0
  38. pyfunda-3.0.0/tests/test_enrichment_parser.py +145 -0
  39. pyfunda-3.0.0/tests/test_live.py +52 -0
  40. pyfunda-3.0.0/tests/test_models.py +46 -0
  41. pyfunda-3.0.0/tests/test_search.py +59 -0
  42. pyfunda-3.0.0/tests/test_transport_parallel.py +122 -0
  43. pyfunda-2.9.0/PKG-INFO +0 -717
  44. pyfunda-2.9.0/README.md +0 -693
  45. pyfunda-2.9.0/examples/analysis.ipynb +0 -215
  46. pyfunda-2.9.0/examples/export_to_csv.py +0 -127
  47. pyfunda-2.9.0/examples/new_listings_alert.py +0 -117
  48. pyfunda-2.9.0/examples/poll_new_listings.py +0 -62
  49. pyfunda-2.9.0/examples/price_history.py +0 -79
  50. pyfunda-2.9.0/examples/price_tracker.py +0 -124
  51. pyfunda-2.9.0/examples/search_sold.py +0 -90
  52. pyfunda-2.9.0/funda/__init__.py +0 -22
  53. pyfunda-2.9.0/funda/funda.py +0 -1435
  54. pyfunda-2.9.0/funda/listing.py +0 -138
  55. pyfunda-2.9.0/test_all_flows.py +0 -1160
  56. {pyfunda-2.9.0 → pyfunda-3.0.0}/.dockerignore +0 -0
  57. {pyfunda-2.9.0 → pyfunda-3.0.0}/.github/FUNDING.yml +0 -0
  58. {pyfunda-2.9.0 → pyfunda-3.0.0}/.github/workflows/publish.yml +0 -0
  59. {pyfunda-2.9.0 → pyfunda-3.0.0}/.gitignore +0 -0
  60. {pyfunda-2.9.0 → pyfunda-3.0.0}/Dockerfile +0 -0
  61. {pyfunda-2.9.0 → pyfunda-3.0.0}/LICENSE +0 -0
  62. {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)
@@ -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.