pyfunda 2.2.0__tar.gz → 2.3.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.
@@ -1,7 +1,9 @@
1
1
  name: Publish to PyPI
2
2
 
3
3
  on:
4
- workflow_dispatch:
4
+ push:
5
+ branches: [main]
6
+ paths: [pyproject.toml]
5
7
 
6
8
  jobs:
7
9
  publish:
@@ -18,13 +20,27 @@ jobs:
18
20
  id: version
19
21
  run: echo "version=$(grep '^version' pyproject.toml | cut -d'"' -f2)" >> $GITHUB_OUTPUT
20
22
 
23
+ - name: Check if release exists
24
+ id: check
25
+ run: |
26
+ if gh release view "v${{ steps.version.outputs.version }}" &>/dev/null; then
27
+ echo "exists=true" >> $GITHUB_OUTPUT
28
+ else
29
+ echo "exists=false" >> $GITHUB_OUTPUT
30
+ fi
31
+ env:
32
+ GH_TOKEN: ${{ github.token }}
33
+
21
34
  - name: Build
35
+ if: steps.check.outputs.exists == 'false'
22
36
  run: uv build
23
37
 
24
38
  - name: Publish to PyPI
39
+ if: steps.check.outputs.exists == 'false'
25
40
  run: uv publish --token ${{ secrets.PYPI_TOKEN }}
26
41
 
27
42
  - name: Create GitHub Release
43
+ if: steps.check.outputs.exists == 'false'
28
44
  run: gh release create "v${{ steps.version.outputs.version }}" --title "v${{ steps.version.outputs.version }}" --generate-notes
29
45
  env:
30
46
  GH_TOKEN: ${{ github.token }}
@@ -1,3 +1,27 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyfunda
3
+ Version: 2.3.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
+ Requires-Python: >=3.10
20
+ Requires-Dist: curl-cffi>=0.14.0
21
+ Requires-Dist: tls-client>=1.0.1
22
+ Requires-Dist: typing-extensions>=4.0.0
23
+ Description-Content-Type: text/markdown
24
+
1
25
  # pyfunda
2
26
 
3
27
  [![PyPI version](https://img.shields.io/pypi/v/pyfunda)](https://pypi.org/project/pyfunda/)
@@ -10,11 +34,17 @@ The only working real Python API for Funda ([funda.nl](https://www.funda.nl))
10
34
 
11
35
  [![Star History Chart](https://api.star-history.com/svg?repos=0xMH/pyfunda&type=Date)](https://star-history.com/#0xMH/pyfunda&Date)
12
36
 
13
- ## Installation
37
+ ## Why I'm open-sourcing this?
14
38
 
15
- ```bash
16
- pip install pyfunda
17
- ```
39
+ After pyfunda, I got messages asking why I'd give this away when aggregators will just take it and sell it. They're right, every week there's a new "revolutionary AI-powered housing finder" charging €40/month or a €250 "success fee.". They all pull from the same one or two sources and wrap it in a fancy UI completely built with AI.
40
+
41
+ That's exactly why I'm open-sourcing it.
42
+
43
+ These services are selling air to people who are looking for any kind of hope. The data is public. The APIs aren't hard to figure out. You shouldn't have to pay someone to refresh a webpage for you. Funda could kill this entire market overnight by offering a public API. They don't, so here we are.
44
+
45
+ Here's the code, do it yourself. Send my library link to any AI service you use and ask it to build whatever tool you think will make your life easier while searching for your next home.
46
+
47
+ With pyfunda, I've already done all the heavy lifting for you.
18
48
 
19
49
  ## Why pyfunda?
20
50
 
@@ -36,6 +66,12 @@ Funda has no public API. If you want Dutch real estate data programmatically, yo
36
66
  - 70+ fields including photos, floorplans, coordinates, and listing dates
37
67
  - Stable mobile API that doesn't break when the website changes
38
68
 
69
+ ## Installation
70
+
71
+ ```bash
72
+ pip install pyfunda
73
+ ```
74
+
39
75
  ## Quick Start
40
76
 
41
77
  ```python
@@ -545,6 +581,18 @@ if len(funda_prices) >= 2:
545
581
  print(f"\nPrice change: {change_pct:+.1f}%")
546
582
  ```
547
583
 
584
+ ## Disclaimer
585
+
586
+ This is an unofficial library and is not affiliated with, authorized, maintained, sponsored, or endorsed by Funda or any of its affiliates. Use at your own risk.
587
+
588
+ This library only accesses publicly available listing data through Funda's undocumented internal API. Using this library may violate Funda's Terms of Service. The authors are not responsible for any consequences of using this software.
589
+
590
+ This project is intended for personal use, research, and educational purposes only.
591
+
592
+ - The API is undocumented and may change or break at any time without notice.
593
+ - Please use this library responsibly and avoid excessive requests that could burden Funda's infrastructure.
594
+ - Scraped data may be subject to copyright and usage restrictions. Ensure your use complies with applicable laws.
595
+
548
596
  ## License
549
597
 
550
598
  AGPL-3.0
@@ -1,25 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: pyfunda
3
- Version: 2.2.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
- Requires-Python: >=3.10
20
- Requires-Dist: curl-cffi>=0.14.0
21
- Description-Content-Type: text/markdown
22
-
23
1
  # pyfunda
24
2
 
25
3
  [![PyPI version](https://img.shields.io/pypi/v/pyfunda)](https://pypi.org/project/pyfunda/)
@@ -32,11 +10,17 @@ The only working real Python API for Funda ([funda.nl](https://www.funda.nl))
32
10
 
33
11
  [![Star History Chart](https://api.star-history.com/svg?repos=0xMH/pyfunda&type=Date)](https://star-history.com/#0xMH/pyfunda&Date)
34
12
 
35
- ## Installation
13
+ ## Why I'm open-sourcing this?
36
14
 
37
- ```bash
38
- pip install pyfunda
39
- ```
15
+ After pyfunda, I got messages asking why I'd give this away when aggregators will just take it and sell it. They're right, every week there's a new "revolutionary AI-powered housing finder" charging €40/month or a €250 "success fee.". They all pull from the same one or two sources and wrap it in a fancy UI completely built with AI.
16
+
17
+ That's exactly why I'm open-sourcing it.
18
+
19
+ These services are selling air to people who are looking for any kind of hope. The data is public. The APIs aren't hard to figure out. You shouldn't have to pay someone to refresh a webpage for you. Funda could kill this entire market overnight by offering a public API. They don't, so here we are.
20
+
21
+ Here's the code, do it yourself. Send my library link to any AI service you use and ask it to build whatever tool you think will make your life easier while searching for your next home.
22
+
23
+ With pyfunda, I've already done all the heavy lifting for you.
40
24
 
41
25
  ## Why pyfunda?
42
26
 
@@ -58,6 +42,12 @@ Funda has no public API. If you want Dutch real estate data programmatically, yo
58
42
  - 70+ fields including photos, floorplans, coordinates, and listing dates
59
43
  - Stable mobile API that doesn't break when the website changes
60
44
 
45
+ ## Installation
46
+
47
+ ```bash
48
+ pip install pyfunda
49
+ ```
50
+
61
51
  ## Quick Start
62
52
 
63
53
  ```python
@@ -567,6 +557,18 @@ if len(funda_prices) >= 2:
567
557
  print(f"\nPrice change: {change_pct:+.1f}%")
568
558
  ```
569
559
 
560
+ ## Disclaimer
561
+
562
+ This is an unofficial library and is not affiliated with, authorized, maintained, sponsored, or endorsed by Funda or any of its affiliates. Use at your own risk.
563
+
564
+ This library only accesses publicly available listing data through Funda's undocumented internal API. Using this library may violate Funda's Terms of Service. The authors are not responsible for any consequences of using this software.
565
+
566
+ This project is intended for personal use, research, and educational purposes only.
567
+
568
+ - The API is undocumented and may change or break at any time without notice.
569
+ - Please use this library responsibly and avoid excessive requests that could burden Funda's infrastructure.
570
+ - Scraped data may be subject to copyright and usage restrictions. Ensure your use complies with applicable laws.
571
+
570
572
  ## License
571
573
 
572
574
  AGPL-3.0
@@ -13,8 +13,10 @@ Example usage:
13
13
  ... print(r['title'], r['city'])
14
14
  """
15
15
 
16
+ from importlib.metadata import version
17
+
16
18
  from funda.funda import Funda, FundaAPI
17
19
  from funda.listing import Listing
18
20
 
19
- __version__ = "2.2.0"
21
+ __version__ = version("pyfunda")
20
22
  __all__ = ["Funda", "FundaAPI", "Listing", "__version__"]
@@ -1,10 +1,12 @@
1
1
  """Main Funda API class."""
2
2
 
3
+ import random
3
4
  import re
4
5
  import time
5
6
  from typing import Any
6
7
 
7
8
  from curl_cffi import requests
9
+ import tls_client
8
10
 
9
11
  from funda.listing import Listing
10
12
 
@@ -16,17 +18,71 @@ API_LISTING_TINY = f"{API_BASE}/tinyId/{{tiny_id}}"
16
18
  API_SEARCH = "https://listing-search-wonen.funda.io/_msearch/template"
17
19
  API_WALTER = "https://api.walterliving.com/hunter/lookup"
18
20
 
19
- # Headers for mobile API
20
- HEADERS = {
21
- "x-funda-app-platform": "android",
22
- "content-type": "application/json",
23
- }
21
+ # Funda mobile app JA3 fingerprints (captured from real Dart/Flutter app traffic)
22
+ # JA3 without extension 21 - from favourites.funda.io
23
+ # JA3 hash: 9225d95490794840d9d5f1f94d339285
24
+ FUNDA_JA3 = "771,4867-4865-4866-52393-52392-49195-49199-49196-49200-49161-49171-49162-49172-156-157-47-53,0-23-65281-10-11-35-13-51-45-43,29-23-24,0"
24
25
 
25
- SEARCH_HEADERS = {
26
- "content-type": "application/json",
27
- "accept": "application/json",
28
- "referer": "https://www.funda.nl/",
29
- }
26
+ # JA3 with extension 21 (padding) - from cdn-settings.segment.com
27
+ # JA3 hash: 4bf8cdd8919b07d35ca824c20efb3537
28
+ FUNDA_JA3_EXT21 = "771,4867-4865-4866-52393-52392-49195-49199-49196-49200-49161-49171-49162-49172-156-157-47-53,0-23-65281-10-11-35-13-51-45-43-21,29-23-24,0"
29
+
30
+ # Fingerprint pool - tried in order until one works
31
+ # Types: "tls_ja3", "curl_impersonate", "tls_client"
32
+ FINGERPRINT_POOL = [
33
+ # tls_client with exact Funda app JA3 fingerprints
34
+ {"type": "tls_ja3", "ja3": FUNDA_JA3},
35
+ {"type": "tls_ja3", "ja3": FUNDA_JA3_EXT21},
36
+ # curl_cffi impersonate fallback - Safari works best with Funda headers
37
+ {"type": "curl_impersonate", "target": "safari15_5"},
38
+ {"type": "curl_impersonate", "target": "safari15_3"},
39
+ # tls_client preset profiles as final fallback
40
+ {"type": "tls_client", "identifier": "okhttp4_android_13"},
41
+ {"type": "tls_client", "identifier": "chrome_120"},
42
+ ]
43
+
44
+ # Test endpoint to verify fingerprint works
45
+ TEST_URL = f"{API_BASE}/tinyId/43117443"
46
+
47
+
48
+
49
+ def _make_headers(for_search: bool = False) -> list[tuple[str, str]]:
50
+ """Generate headers matching the Funda Android app exactly.
51
+
52
+ Header order and values are captured from real Funda app traffic.
53
+ """
54
+ trace_id = str(random.randint(10**18, 10**19))
55
+ parent_id = hex(random.randint(10**15, 10**16))[2:]
56
+ tid = hex(int(time.time()))[2:] + "00000000"
57
+
58
+ # Base headers in exact order from app traffic
59
+ headers = [
60
+ ("user-agent", "Dart/3.9 (dart:io)"),
61
+ ("x-datadog-sampling-priority", "0"),
62
+ ("x-datadog-origin", "rum"),
63
+ ("tracestate", f"dd=s:0;o:rum;p:{parent_id}"),
64
+ ("accept-encoding", "gzip"),
65
+ ("x-datadog-parent-id", trace_id),
66
+ ]
67
+
68
+ if for_search:
69
+ # Search endpoint: content-type, referer, accept, then traceparent
70
+ headers.extend([
71
+ ("content-type", "application/json"),
72
+ ("referer", "https://www.funda.nl/"),
73
+ ("accept", "application/json"),
74
+ ])
75
+ else:
76
+ # Listing endpoint: x-funda-app-platform, content-type, then traceparent
77
+ headers.extend([
78
+ ("x-funda-app-platform", "android"),
79
+ ("content-type", "application/json"),
80
+ ])
81
+
82
+ # traceparent is always last
83
+ headers.append(("traceparent", f"00-{tid}{trace_id[:16]}-{parent_id}-00"))
84
+
85
+ return headers
30
86
 
31
87
 
32
88
  def _parse_area(value: str | None) -> int | None:
@@ -63,21 +119,145 @@ class Funda:
63
119
  timeout: Request timeout in seconds
64
120
  """
65
121
  self.timeout = timeout
66
- self._session: requests.Session | None = None
122
+ self._curl_session: requests.Session | None = None
123
+ self._tls_session: tls_client.Session | None = None
124
+ self._fingerprint: dict | None = None
67
125
 
68
- @property
69
- def session(self) -> requests.Session:
70
- """Lazily create HTTP session."""
71
- if self._session is None:
72
- self._session = requests.Session(impersonate="chrome")
73
- self._session.headers.update(HEADERS)
74
- return self._session
126
+ def _make_headers_dict(self, for_search: bool = False) -> dict[str, str]:
127
+ """Generate headers as dict for tls_client.
128
+
129
+ Header order and values match the real Funda Android app exactly.
130
+ """
131
+ trace_id = str(random.randint(10**18, 10**19))
132
+ parent_id = hex(random.randint(10**15, 10**16))[2:]
133
+ tid = hex(int(time.time()))[2:] + "00000000"
134
+
135
+ # Build headers in exact order from app traffic
136
+ # Python 3.7+ dicts preserve insertion order
137
+ headers = {
138
+ "user-agent": "Dart/3.9 (dart:io)",
139
+ "x-datadog-sampling-priority": "0",
140
+ "x-datadog-origin": "rum",
141
+ "tracestate": f"dd=s:0;o:rum;p:{parent_id}",
142
+ "accept-encoding": "gzip",
143
+ "x-datadog-parent-id": trace_id,
144
+ }
145
+
146
+ if for_search:
147
+ # Search endpoint: content-type, referer, accept, then traceparent
148
+ headers["content-type"] = "application/json"
149
+ headers["referer"] = "https://www.funda.nl/"
150
+ headers["accept"] = "application/json"
151
+ else:
152
+ # Listing endpoint: x-funda-app-platform, content-type, then traceparent
153
+ headers["x-funda-app-platform"] = "android"
154
+ headers["content-type"] = "application/json"
155
+
156
+ # traceparent is always last
157
+ headers["traceparent"] = f"00-{tid}{trace_id[:16]}-{parent_id}-00"
158
+
159
+ return headers
160
+
161
+ def _test_fingerprint(self, fingerprint: dict) -> bool:
162
+ """Test if a fingerprint works against Funda API."""
163
+ try:
164
+ fp_type = fingerprint["type"]
165
+ if fp_type == "tls_ja3":
166
+ # tls_client with custom JA3 - primary method for Funda app fingerprint
167
+ session = tls_client.Session(ja3_string=fingerprint["ja3"], random_tls_extension_order=False)
168
+ headers = self._make_headers_dict()
169
+ response = session.get(TEST_URL, headers=headers, timeout_seconds=5)
170
+ elif fp_type == "curl_ja3":
171
+ session = requests.Session()
172
+ headers = _make_headers()
173
+ response = session.get(TEST_URL, headers=headers, ja3=fingerprint["ja3"], timeout=5)
174
+ session.close()
175
+ elif fp_type == "curl_impersonate":
176
+ session = requests.Session(impersonate=fingerprint["target"])
177
+ headers = _make_headers()
178
+ response = session.get(TEST_URL, headers=headers, timeout=5)
179
+ session.close()
180
+ elif fp_type == "tls_client":
181
+ session = tls_client.Session(client_identifier=fingerprint["identifier"], random_tls_extension_order=False)
182
+ headers = self._make_headers_dict()
183
+ response = session.get(TEST_URL, headers=headers, timeout_seconds=5)
184
+ else:
185
+ return False
186
+ return response.status_code == 200
187
+ except Exception:
188
+ return False
189
+
190
+ def _find_working_fingerprint(self) -> dict:
191
+ """Find a working fingerprint from the pool."""
192
+ for fp in FINGERPRINT_POOL:
193
+ if self._test_fingerprint(fp):
194
+ return fp
195
+ raise RuntimeError("No working fingerprint found. Funda may have updated their bot detection.")
196
+
197
+ def _ensure_session(self) -> None:
198
+ """Ensure a working session is created."""
199
+ if self._fingerprint is None:
200
+ self._fingerprint = self._find_working_fingerprint()
201
+
202
+ fp_type = self._fingerprint["type"]
203
+ if fp_type == "tls_ja3":
204
+ # tls_client with custom JA3 - exact Funda app fingerprint
205
+ if self._tls_session is None:
206
+ self._tls_session = tls_client.Session(
207
+ ja3_string=self._fingerprint["ja3"],
208
+ random_tls_extension_order=False
209
+ )
210
+ elif fp_type == "curl_ja3":
211
+ if self._curl_session is None:
212
+ self._curl_session = requests.Session()
213
+ elif fp_type == "curl_impersonate":
214
+ if self._curl_session is None:
215
+ self._curl_session = requests.Session(impersonate=self._fingerprint["target"])
216
+ elif fp_type == "tls_client":
217
+ if self._tls_session is None:
218
+ self._tls_session = tls_client.Session(
219
+ client_identifier=self._fingerprint["identifier"],
220
+ random_tls_extension_order=False
221
+ )
222
+
223
+ def _get(self, url: str, headers_list: list[tuple[str, str]]) -> Any:
224
+ """Make GET request using the active session."""
225
+ self._ensure_session()
226
+ fp_type = self._fingerprint["type"]
227
+
228
+ if fp_type in ("tls_ja3", "tls_client"):
229
+ headers = self._make_headers_dict()
230
+ return self._tls_session.get(url, headers=headers, timeout_seconds=self.timeout)
231
+ elif fp_type == "curl_ja3":
232
+ return self._curl_session.get(url, headers=headers_list, ja3=self._fingerprint["ja3"], timeout=self.timeout)
233
+ else:
234
+ return self._curl_session.get(url, headers=headers_list, timeout=self.timeout)
235
+
236
+ def _post(self, url: str, headers_list: list[tuple[str, str]], data: str = None, json_data: dict = None, for_search: bool = False) -> Any:
237
+ """Make POST request using the active session."""
238
+ self._ensure_session()
239
+ fp_type = self._fingerprint["type"]
240
+
241
+ if fp_type in ("tls_ja3", "tls_client"):
242
+ headers = self._make_headers_dict(for_search=for_search)
243
+ if json_data:
244
+ return self._tls_session.post(url, headers=headers, json=json_data, timeout_seconds=self.timeout)
245
+ return self._tls_session.post(url, headers=headers, data=data, timeout_seconds=self.timeout)
246
+ elif fp_type == "curl_ja3":
247
+ if json_data:
248
+ return self._curl_session.post(url, headers=headers_list, json=json_data, ja3=self._fingerprint["ja3"], timeout=self.timeout)
249
+ return self._curl_session.post(url, headers=headers_list, data=data, ja3=self._fingerprint["ja3"], timeout=self.timeout)
250
+ else:
251
+ if json_data:
252
+ return self._curl_session.post(url, headers=headers_list, json=json_data, timeout=self.timeout)
253
+ return self._curl_session.post(url, headers=headers_list, data=data, timeout=self.timeout)
75
254
 
76
255
  def close(self) -> None:
77
256
  """Close the HTTP session."""
78
- if self._session:
79
- self._session.close()
80
- self._session = None
257
+ if self._curl_session:
258
+ self._curl_session.close()
259
+ self._curl_session = None
260
+ self._tls_session = None
81
261
 
82
262
  def __enter__(self) -> "Funda":
83
263
  return self
@@ -116,12 +296,14 @@ class Funda:
116
296
  else:
117
297
  url = API_LISTING.format(listing_id=listing_id_str)
118
298
 
119
- response = self.session.get(url, timeout=self.timeout)
299
+ headers = _make_headers()
300
+ response = self._get(url, headers)
120
301
 
121
302
  # If tinyId fails, try as globalId
122
303
  if response.status_code == 404 and len(listing_id_str) >= 8:
123
304
  url = API_LISTING.format(listing_id=listing_id_str)
124
- response = self.session.get(url, timeout=self.timeout)
305
+ headers = _make_headers()
306
+ response = self._get(url, headers)
125
307
 
126
308
  if response.status_code != 200:
127
309
  raise LookupError(f"Listing {listing_id} not found")
@@ -260,12 +442,8 @@ class Funda:
260
442
 
261
443
  # Retry on intermittent 400 errors from API
262
444
  for attempt in range(3):
263
- response = self.session.post(
264
- API_SEARCH,
265
- headers=SEARCH_HEADERS,
266
- data=query,
267
- timeout=self.timeout,
268
- )
445
+ headers = _make_headers(for_search=True)
446
+ response = self._post(API_SEARCH, headers, data=query, for_search=True)
269
447
  if response.status_code == 200:
270
448
  break
271
449
  if response.status_code == 400 and attempt < 2:
@@ -462,7 +640,8 @@ class Funda:
462
640
  while consecutive_404s < max_consecutive_404s:
463
641
  url = API_LISTING.format(listing_id=current_id)
464
642
  try:
465
- response = self.session.get(url, timeout=self.timeout)
643
+ headers = _make_headers()
644
+ response = self._get(url, headers)
466
645
 
467
646
  if response.status_code == 200:
468
647
  consecutive_404s = 0
@@ -480,7 +659,7 @@ class Funda:
480
659
  else:
481
660
  consecutive_404s += 1
482
661
 
483
- except requests.RequestException:
662
+ except requests.errors.RequestsError:
484
663
  consecutive_404s += 1
485
664
 
486
665
  current_id += 1
@@ -531,12 +710,11 @@ class Funda:
531
710
  "zipcode": postcode,
532
711
  }
533
712
 
534
- response = self.session.post(
535
- API_WALTER,
536
- json=payload,
537
- headers={"Accept": "application/json", "Content-Type": "application/json"},
538
- timeout=self.timeout,
539
- )
713
+ walter_headers = [
714
+ ("Accept", "application/json"),
715
+ ("Content-Type", "application/json"),
716
+ ]
717
+ response = self._post(API_WALTER, walter_headers, json_data=payload)
540
718
 
541
719
  if response.status_code != 200:
542
720
  raise LookupError(f"Could not fetch price history (status {response.status_code})")
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pyfunda"
7
- version = "2.2.0"
7
+ version = "2.3.0"
8
8
  description = "Python API for Funda.nl real estate listings"
9
9
  readme = "README.md"
10
10
  license = "AGPL-3.0-or-later"
@@ -24,6 +24,8 @@ classifiers = [
24
24
  ]
25
25
  dependencies = [
26
26
  "curl-cffi>=0.14.0",
27
+ "tls-client>=1.0.1",
28
+ "typing_extensions>=4.0.0",
27
29
  ]
28
30
 
29
31
  [project.urls]
@@ -626,18 +626,18 @@ def test_session_lazy_loading():
626
626
  print(" Session lazy loading working correctly")
627
627
 
628
628
 
629
- @test("Session headers are set correctly")
629
+ @test("Session is created correctly")
630
630
  def test_session_headers():
631
631
  from funda import Funda
632
632
 
633
633
  f = Funda()
634
634
  session = f.session
635
635
 
636
- assert 'user-agent' in session.headers, "Should have user-agent header"
637
- assert 'Dart' in session.headers['user-agent'], "User-agent should contain 'Dart'"
636
+ # Session exists (headers are generated per-request with Dart fingerprint)
637
+ assert session is not None, "Session should exist"
638
638
 
639
639
  f.close()
640
- print(" Session headers set correctly")
640
+ print(" Session created correctly")
641
641
 
642
642
 
643
643
  @test("Custom timeout is respected")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes