pyfunda 2.2.0__tar.gz → 2.2.1__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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyfunda
3
- Version: 2.2.0
3
+ Version: 2.2.1
4
4
  Summary: Python API for Funda.nl real estate listings
5
5
  Project-URL: Homepage, https://github.com/0xMH/pyfunda
6
6
  Project-URL: Repository, https://github.com/0xMH/pyfunda
@@ -567,6 +567,18 @@ if len(funda_prices) >= 2:
567
567
  print(f"\nPrice change: {change_pct:+.1f}%")
568
568
  ```
569
569
 
570
+ ## Disclaimer
571
+
572
+ 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.
573
+
574
+ 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.
575
+
576
+ This project is intended for personal use, research, and educational purposes only.
577
+
578
+ - The API is undocumented and may change or break at any time without notice.
579
+ - Please use this library responsibly and avoid excessive requests that could burden Funda's infrastructure.
580
+ - Scraped data may be subject to copyright and usage restrictions. Ensure your use complies with applicable laws.
581
+
570
582
  ## License
571
583
 
572
584
  AGPL-3.0
@@ -545,6 +545,18 @@ if len(funda_prices) >= 2:
545
545
  print(f"\nPrice change: {change_pct:+.1f}%")
546
546
  ```
547
547
 
548
+ ## Disclaimer
549
+
550
+ 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.
551
+
552
+ 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.
553
+
554
+ This project is intended for personal use, research, and educational purposes only.
555
+
556
+ - The API is undocumented and may change or break at any time without notice.
557
+ - Please use this library responsibly and avoid excessive requests that could burden Funda's infrastructure.
558
+ - Scraped data may be subject to copyright and usage restrictions. Ensure your use complies with applicable laws.
559
+
548
560
  ## License
549
561
 
550
562
  AGPL-3.0
@@ -16,5 +16,5 @@ Example usage:
16
16
  from funda.funda import Funda, FundaAPI
17
17
  from funda.listing import Listing
18
18
 
19
- __version__ = "2.2.0"
19
+ __version__ = "2.2.1"
20
20
  __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
+ from curl_cffi.const import CurlHttpVersion
8
10
 
9
11
  from funda.listing import Listing
10
12
 
@@ -16,17 +18,47 @@ 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
- }
24
-
25
- SEARCH_HEADERS = {
26
- "content-type": "application/json",
27
- "accept": "application/json",
28
- "referer": "https://www.funda.nl/",
29
- }
21
+ 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-21,29-23-24,0"
22
+
23
+
24
+
25
+ def _make_headers(host: str, for_search: bool = False) -> list[tuple[str, str]]:
26
+ """Generate headers matching the Funda Android app."""
27
+ trace_id = str(random.randint(10**18, 10**19))
28
+ parent_id = hex(random.randint(10**15, 10**16))[2:]
29
+ tid = hex(int(time.time()))[2:] + "00000000"
30
+
31
+ headers = [
32
+ ("user-agent", "Dart/3.9 (dart:io)"),
33
+ ("x-datadog-sampling-priority", "0"),
34
+ ("x-datadog-origin", "rum"),
35
+ ("tracestate", f"dd=s:0;o:rum;p:{parent_id}"),
36
+ ("accept-encoding", "gzip"),
37
+ ("x-datadog-parent-id", trace_id),
38
+ ]
39
+
40
+ if for_search:
41
+ # Search endpoint uses referer and accept instead of x-funda-app-platform
42
+ headers.extend([
43
+ ("content-type", "application/json"),
44
+ ("referer", "https://www.funda.nl/"),
45
+ ("accept", "application/json"),
46
+ ])
47
+ else:
48
+ # Listing endpoint uses x-funda-app-platform
49
+ headers.extend([
50
+ ("x-funda-app-platform", "android"),
51
+ ("content-type", "application/json"),
52
+ ])
53
+
54
+ headers.extend([
55
+ ("traceparent", f"00-{tid}{trace_id[:16]}-{parent_id}-00"),
56
+ ("host", host),
57
+ ("x-datadog-tags", f"_dd.p.tid={tid}"),
58
+ ("x-datadog-trace-id", trace_id),
59
+ ])
60
+
61
+ return headers
30
62
 
31
63
 
32
64
  def _parse_area(value: str | None) -> int | None:
@@ -69,8 +101,7 @@ class Funda:
69
101
  def session(self) -> requests.Session:
70
102
  """Lazily create HTTP session."""
71
103
  if self._session is None:
72
- self._session = requests.Session(impersonate="chrome")
73
- self._session.headers.update(HEADERS)
104
+ self._session = requests.Session()
74
105
  return self._session
75
106
 
76
107
  def close(self) -> None:
@@ -111,17 +142,26 @@ class Funda:
111
142
 
112
143
  # Try tinyId endpoint first (8-9 digits), then globalId (7 digits)
113
144
  listing_id_str = str(listing_id)
145
+ host = "listing-detail-page.funda.io"
114
146
  if len(listing_id_str) >= 8:
115
147
  url = API_LISTING_TINY.format(tiny_id=listing_id_str)
116
148
  else:
117
149
  url = API_LISTING.format(listing_id=listing_id_str)
118
150
 
119
- response = self.session.get(url, timeout=self.timeout)
151
+ headers = _make_headers(host)
152
+ response = self.session.get(
153
+ url, headers=headers, ja3=FUNDA_JA3,
154
+ http_version=CurlHttpVersion.V1_1, timeout=self.timeout
155
+ )
120
156
 
121
157
  # If tinyId fails, try as globalId
122
158
  if response.status_code == 404 and len(listing_id_str) >= 8:
123
159
  url = API_LISTING.format(listing_id=listing_id_str)
124
- response = self.session.get(url, timeout=self.timeout)
160
+ headers = _make_headers(host)
161
+ response = self.session.get(
162
+ url, headers=headers, ja3=FUNDA_JA3,
163
+ http_version=CurlHttpVersion.V1_1, timeout=self.timeout
164
+ )
125
165
 
126
166
  if response.status_code != 200:
127
167
  raise LookupError(f"Listing {listing_id} not found")
@@ -259,11 +299,15 @@ class Funda:
259
299
  query = f"{index_line}\n{query_line}\n"
260
300
 
261
301
  # Retry on intermittent 400 errors from API
302
+ host = "listing-search-wonen.funda.io"
262
303
  for attempt in range(3):
304
+ headers = _make_headers(host, for_search=True)
263
305
  response = self.session.post(
264
306
  API_SEARCH,
265
- headers=SEARCH_HEADERS,
307
+ headers=headers,
266
308
  data=query,
309
+ ja3=FUNDA_JA3,
310
+ http_version=CurlHttpVersion.V1_1,
267
311
  timeout=self.timeout,
268
312
  )
269
313
  if response.status_code == 200:
@@ -458,11 +502,16 @@ class Funda:
458
502
  """
459
503
  consecutive_404s = 0
460
504
  current_id = since_id + 1
505
+ host = "listing-detail-page.funda.io"
461
506
 
462
507
  while consecutive_404s < max_consecutive_404s:
463
508
  url = API_LISTING.format(listing_id=current_id)
464
509
  try:
465
- response = self.session.get(url, timeout=self.timeout)
510
+ headers = _make_headers(host)
511
+ response = self.session.get(
512
+ url, headers=headers, ja3=FUNDA_JA3,
513
+ http_version=CurlHttpVersion.V1_1, timeout=self.timeout
514
+ )
466
515
 
467
516
  if response.status_code == 200:
468
517
  consecutive_404s = 0
@@ -536,6 +585,7 @@ class Funda:
536
585
  json=payload,
537
586
  headers={"Accept": "application/json", "Content-Type": "application/json"},
538
587
  timeout=self.timeout,
588
+ http_version=CurlHttpVersion.V1_1,
539
589
  )
540
590
 
541
591
  if response.status_code != 200:
@@ -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.2.1"
8
8
  description = "Python API for Funda.nl real estate listings"
9
9
  readme = "README.md"
10
10
  license = "AGPL-3.0-or-later"
@@ -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