pyfunda 3.1.0__tar.gz → 3.1.2__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.1.0 → pyfunda-3.1.2}/PKG-INFO +9 -5
- {pyfunda-3.1.0 → pyfunda-3.1.2}/README.md +6 -2
- pyfunda-3.1.0/funda/autocomplete.py → pyfunda-3.1.2/funda/_autocomplete.py +2 -2
- {pyfunda-3.1.0 → pyfunda-3.1.2}/funda/constants.py +1 -1
- {pyfunda-3.1.0 → pyfunda-3.1.2}/funda/funda.py +31 -6
- {pyfunda-3.1.0 → pyfunda-3.1.2}/funda/headers.py +2 -1
- {pyfunda-3.1.0 → pyfunda-3.1.2}/pyproject.toml +22 -3
- {pyfunda-3.1.0 → pyfunda-3.1.2}/tests/test_autocomplete.py +5 -5
- {pyfunda-3.1.0 → pyfunda-3.1.2}/tests/test_client.py +24 -2
- {pyfunda-3.1.0 → pyfunda-3.1.2}/tests/test_live.py +20 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/tests/test_search.py +3 -2
- {pyfunda-3.1.0 → pyfunda-3.1.2}/.dockerignore +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/.github/FUNDING.yml +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/.github/workflows/publish.yml +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/.gitignore +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/Dockerfile +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/LICENSE +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/docs/API.md +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/docs/ARCHITECTURE.md +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/docs/DEVELOPMENT.md +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/docs/EXAMPLES.md +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/docs/README.md +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/examples/almere_age_rank.py +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/examples/analysis.ipynb +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/examples/batch_details.py +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/examples/broker_due_diligence.py +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/examples/enrichment_export.py +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/examples/export_to_csv.py +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/examples/full_api_walkthrough.py +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/examples/neighborhood_market_snapshot.py +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/examples/new_listings_alert.py +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/examples/poll_new_listings.py +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/examples/price_history.py +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/examples/price_tracker.py +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/examples/search_sold.py +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/examples/similar_sales_comp.py +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/funda/__init__.py +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/funda/_autocomplete_parser.py +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/funda/_detail_parser.py +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/funda/_enrichment_parser.py +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/funda/_parallel.py +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/funda/_parse_helpers.py +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/funda/_price_history_parser.py +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/funda/_search_parser.py +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/funda/_transport.py +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/funda/exceptions.py +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/funda/listing.py +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/funda/models.py +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/funda/parsing.py +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/funda/py.typed +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/funda/search.py +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/tests/__init__.py +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/tests/test_enrichment_parser.py +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/tests/test_models.py +0 -0
- {pyfunda-3.1.0 → pyfunda-3.1.2}/tests/test_transport_parallel.py +0 -0
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyfunda
|
|
3
|
-
Version: 3.1.
|
|
4
|
-
Summary: Python API for Funda.nl real estate
|
|
3
|
+
Version: 3.1.2
|
|
4
|
+
Summary: Python API wrapper for Funda.nl, the Dutch real estate platform. Reverse-engineered mobile API client, no scraping, no Selenium, no CAPTCHA.
|
|
5
5
|
Project-URL: Homepage, https://github.com/0xMH/pyfunda
|
|
6
6
|
Project-URL: Repository, https://github.com/0xMH/pyfunda
|
|
7
7
|
Project-URL: Issues, https://github.com/0xMH/pyfunda/issues
|
|
8
8
|
Author: 0xMH
|
|
9
9
|
License-Expression: AGPL-3.0-or-later
|
|
10
10
|
License-File: LICENSE
|
|
11
|
-
Keywords: api,funda,housing,netherlands,real-estate,scraper
|
|
11
|
+
Keywords: api,api-wrapper,client,dutch,funda,funda-api,funda-nl,funda-scraper,housing,housing-api,mobile-api,netherlands,real-estate,real-estate-api,rest-api,reverse-engineering,scraper,sdk
|
|
12
12
|
Classifier: Development Status :: 4 - Beta
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
14
14
|
Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
|
|
@@ -29,9 +29,13 @@ Description-Content-Type: text/markdown
|
|
|
29
29
|
[](https://pypi.org/project/pyfunda/)
|
|
30
30
|
[](https://github.com/0xMH/pyfunda/blob/main/LICENSE)
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
**pyfunda** is a Python API wrapper for [Funda.nl](https://www.funda.nl), the Netherlands' largest Dutch real estate platform. It talks to Funda's reverse engineered mobile JSON API, so you get clean typed objects for listings, prices, media, brokers, and price history without HTML scraping, Selenium, a headless browser, or CAPTCHA solving.
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
Funda has no public developer API. pyfunda is the only working opensource Python client that hits Funda's app facing `*.funda.io` endpoints directly, the same ones the official Android app uses.
|
|
35
|
+
|
|
36
|
+
Use pyfunda as a Funda API client or Funda Python SDK to fetch Dutch property listings, run search queries by city and price, pull listing detail and media, get broker info and reviews, read price history, and stream new listings. If you have been looking for a working fundascraper alternative or a Netherlands housing API, this is the only Python wrapper hitting the same `*.funda.io` endpoints the Funda Android app uses.
|
|
37
|
+
|
|
38
|
+
> If you find this useful, consider giving it a star, it helps others discover the project.
|
|
35
39
|
|
|
36
40
|
[](https://star-history.com/#0xMH/pyfunda&Date)
|
|
37
41
|
|
|
@@ -4,9 +4,13 @@
|
|
|
4
4
|
[](https://pypi.org/project/pyfunda/)
|
|
5
5
|
[](https://github.com/0xMH/pyfunda/blob/main/LICENSE)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
**pyfunda** is a Python API wrapper for [Funda.nl](https://www.funda.nl), the Netherlands' largest Dutch real estate platform. It talks to Funda's reverse engineered mobile JSON API, so you get clean typed objects for listings, prices, media, brokers, and price history without HTML scraping, Selenium, a headless browser, or CAPTCHA solving.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Funda has no public developer API. pyfunda is the only working opensource Python client that hits Funda's app facing `*.funda.io` endpoints directly, the same ones the official Android app uses.
|
|
10
|
+
|
|
11
|
+
Use pyfunda as a Funda API client or Funda Python SDK to fetch Dutch property listings, run search queries by city and price, pull listing detail and media, get broker info and reviews, read price history, and stream new listings. If you have been looking for a working fundascraper alternative or a Netherlands housing API, this is the only Python wrapper hitting the same `*.funda.io` endpoints the Funda Android app uses.
|
|
12
|
+
|
|
13
|
+
> If you find this useful, consider giving it a star, it helps others discover the project.
|
|
10
14
|
|
|
11
15
|
[](https://star-history.com/#0xMH/pyfunda&Date)
|
|
12
16
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Internal autocomplete payload construction."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -14,7 +14,7 @@ from funda.models import JsonDict
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
@dataclass(frozen=True, slots=True)
|
|
17
|
-
class
|
|
17
|
+
class LocationAutocomplete:
|
|
18
18
|
value: str
|
|
19
19
|
timeout: str = "3s"
|
|
20
20
|
size: int = 10
|
|
@@ -36,7 +36,7 @@ API_BROKER_REVIEWS = (
|
|
|
36
36
|
)
|
|
37
37
|
|
|
38
38
|
SEARCH_INDEX = "listings-wonen-searcher-alias-prod"
|
|
39
|
-
SEARCH_TEMPLATE_ID = "
|
|
39
|
+
SEARCH_TEMPLATE_ID = "search_result_20260227"
|
|
40
40
|
LOCATION_AUTOCOMPLETE_TEMPLATE_ID = "searchbox_20250805"
|
|
41
41
|
LOCATION_AUTOCOMPLETE_AREA_TYPES = (
|
|
42
42
|
"country",
|
|
@@ -28,7 +28,7 @@ from funda.constants import (
|
|
|
28
28
|
PAGE_SIZE,
|
|
29
29
|
)
|
|
30
30
|
from funda.exceptions import FundaRequestError, ListingNotFound, PriceHistoryError, SearchError
|
|
31
|
-
from funda.
|
|
31
|
+
from funda._autocomplete import LocationAutocomplete
|
|
32
32
|
from funda.listing import Listing, LocationSuggestion, PriceHistory
|
|
33
33
|
from funda.models import JsonDict
|
|
34
34
|
from funda.parsing import (
|
|
@@ -63,7 +63,9 @@ class Funda:
|
|
|
63
63
|
|
|
64
64
|
timeout: int = 30
|
|
65
65
|
max_retries: int = DEFAULT_MAX_RETRIES
|
|
66
|
-
retry_backoff: float = 0.
|
|
66
|
+
retry_backoff: float = 0.5
|
|
67
|
+
min_request_interval: float = 0.0
|
|
68
|
+
adaptive_throttle: bool = True
|
|
67
69
|
|
|
68
70
|
_transport: _FundaTransport = field(init=False, repr=False)
|
|
69
71
|
_parallel_runner: _ParallelRunner["Funda"] | None = field(default=None, init=False, repr=False)
|
|
@@ -73,6 +75,8 @@ class Funda:
|
|
|
73
75
|
timeout=self.timeout,
|
|
74
76
|
max_retries=self.max_retries,
|
|
75
77
|
retry_backoff=self.retry_backoff,
|
|
78
|
+
min_request_interval=self.min_request_interval,
|
|
79
|
+
adaptive_throttle=self.adaptive_throttle,
|
|
76
80
|
)
|
|
77
81
|
|
|
78
82
|
def close(self) -> None:
|
|
@@ -84,7 +88,7 @@ class Funda:
|
|
|
84
88
|
def __enter__(self) -> "Funda":
|
|
85
89
|
return self
|
|
86
90
|
|
|
87
|
-
def __exit__(self, *
|
|
91
|
+
def __exit__(self, *_) -> None:
|
|
88
92
|
self.close()
|
|
89
93
|
|
|
90
94
|
def listing(self, listing_id: int | str) -> Listing:
|
|
@@ -130,7 +134,7 @@ class Funda:
|
|
|
130
134
|
sort: Sequence[Any] | None = None,
|
|
131
135
|
) -> list[LocationSuggestion]:
|
|
132
136
|
"""Suggest Funda location identifiers for search-box text."""
|
|
133
|
-
autocomplete =
|
|
137
|
+
autocomplete = LocationAutocomplete(
|
|
134
138
|
value=value,
|
|
135
139
|
size=size,
|
|
136
140
|
timeout=timeout,
|
|
@@ -423,7 +427,9 @@ class Funda:
|
|
|
423
427
|
for attempt in range(3):
|
|
424
428
|
response = self._transport.post(API_SEARCH, profile="search", data=payload)
|
|
425
429
|
if response.status_code == 200:
|
|
426
|
-
|
|
430
|
+
data = response.json()
|
|
431
|
+
self._raise_for_msearch_error(data)
|
|
432
|
+
return data
|
|
427
433
|
if response.status_code == 400 and attempt < 2:
|
|
428
434
|
time.sleep(0.1 * (attempt + 1))
|
|
429
435
|
continue
|
|
@@ -434,7 +440,24 @@ class Funda:
|
|
|
434
440
|
|
|
435
441
|
raise SearchError("Search failed without a response")
|
|
436
442
|
|
|
437
|
-
|
|
443
|
+
@staticmethod
|
|
444
|
+
def _raise_for_msearch_error(data: JsonDict) -> None:
|
|
445
|
+
"""Surface per-query errors that _msearch reports inside an HTTP 200 body.
|
|
446
|
+
|
|
447
|
+
The _msearch/template endpoint answers with HTTP 200 even when the
|
|
448
|
+
individual query fails (for example a rotated/stale search template),
|
|
449
|
+
embedding the failure as ``responses[].error``. Without this check such
|
|
450
|
+
breakage is silently parsed as zero results.
|
|
451
|
+
"""
|
|
452
|
+
responses = data.get("responses") if isinstance(data, dict) else None
|
|
453
|
+
for entry in responses or []:
|
|
454
|
+
if isinstance(entry, dict) and entry.get("error"):
|
|
455
|
+
error = entry["error"]
|
|
456
|
+
reason = error.get("reason") if isinstance(error, dict) else str(error)
|
|
457
|
+
status = entry.get("status", "unknown")
|
|
458
|
+
raise SearchError(f"Search failed (status {status}): {reason}")
|
|
459
|
+
|
|
460
|
+
def _autocomplete(self, autocomplete: LocationAutocomplete) -> JsonDict:
|
|
438
461
|
payload = autocomplete.to_payload()
|
|
439
462
|
for attempt in range(3):
|
|
440
463
|
response = self._transport.post(
|
|
@@ -484,6 +507,8 @@ class Funda:
|
|
|
484
507
|
timeout=self.timeout,
|
|
485
508
|
max_retries=self.max_retries,
|
|
486
509
|
retry_backoff=self.retry_backoff,
|
|
510
|
+
min_request_interval=self.min_request_interval,
|
|
511
|
+
adaptive_throttle=self.adaptive_throttle,
|
|
487
512
|
)
|
|
488
513
|
|
|
489
514
|
def _listing_id_from_input(self, listing_id: int | str) -> str:
|
|
@@ -19,7 +19,7 @@ def make_headers(profile: RequestProfile = "listing") -> HeaderList:
|
|
|
19
19
|
tid = hex(int(time.time()))[2:] + "00000000"
|
|
20
20
|
|
|
21
21
|
headers = [
|
|
22
|
-
("user-agent", "Dart/3.
|
|
22
|
+
("user-agent", "Dart/3.11 (dart:io)"),
|
|
23
23
|
("x-datadog-sampling-priority", "0"),
|
|
24
24
|
("x-datadog-origin", "rum"),
|
|
25
25
|
("tracestate", f"dd=s:0;o:rum;p:{parent_id}"),
|
|
@@ -39,6 +39,7 @@ def make_headers(profile: RequestProfile = "listing") -> HeaderList:
|
|
|
39
39
|
headers.extend(
|
|
40
40
|
[
|
|
41
41
|
("x-funda-app-platform", "android"),
|
|
42
|
+
("x-funda-app-version", "7.14.11"),
|
|
42
43
|
("content-type", "application/json"),
|
|
43
44
|
]
|
|
44
45
|
)
|
|
@@ -4,15 +4,34 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pyfunda"
|
|
7
|
-
version = "3.1.
|
|
8
|
-
description = "Python API for Funda.nl real estate
|
|
7
|
+
version = "3.1.2"
|
|
8
|
+
description = "Python API wrapper for Funda.nl, the Dutch real estate platform. Reverse-engineered mobile API client, no scraping, no Selenium, no CAPTCHA."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "AGPL-3.0-or-later"
|
|
11
11
|
requires-python = ">=3.10"
|
|
12
12
|
authors = [
|
|
13
13
|
{ name = "0xMH" }
|
|
14
14
|
]
|
|
15
|
-
keywords = [
|
|
15
|
+
keywords = [
|
|
16
|
+
"funda",
|
|
17
|
+
"funda-api",
|
|
18
|
+
"funda-nl",
|
|
19
|
+
"funda-scraper",
|
|
20
|
+
"real-estate",
|
|
21
|
+
"real-estate-api",
|
|
22
|
+
"netherlands",
|
|
23
|
+
"dutch",
|
|
24
|
+
"housing",
|
|
25
|
+
"housing-api",
|
|
26
|
+
"api",
|
|
27
|
+
"api-wrapper",
|
|
28
|
+
"rest-api",
|
|
29
|
+
"client",
|
|
30
|
+
"sdk",
|
|
31
|
+
"mobile-api",
|
|
32
|
+
"reverse-engineering",
|
|
33
|
+
"scraper",
|
|
34
|
+
]
|
|
16
35
|
classifiers = [
|
|
17
36
|
"Development Status :: 4 - Beta",
|
|
18
37
|
"Intended Audience :: Developers",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import unittest
|
|
2
2
|
|
|
3
|
-
from funda.
|
|
3
|
+
from funda._autocomplete import LocationAutocomplete
|
|
4
4
|
from funda.constants import (
|
|
5
5
|
LOCATION_AUTOCOMPLETE_AREA_TYPES,
|
|
6
6
|
LOCATION_AUTOCOMPLETE_TEMPLATE_ID,
|
|
@@ -45,7 +45,7 @@ AUTOCOMPLETE_RESPONSE = {
|
|
|
45
45
|
|
|
46
46
|
class AutocompleteTests(unittest.TestCase):
|
|
47
47
|
def test_payload_matches_funda_searchbox_template(self) -> None:
|
|
48
|
-
payload =
|
|
48
|
+
payload = LocationAutocomplete("almere poor").to_payload()
|
|
49
49
|
|
|
50
50
|
self.assertEqual(payload["id"], LOCATION_AUTOCOMPLETE_TEMPLATE_ID)
|
|
51
51
|
self.assertEqual(payload["params"]["value"], "almere poor")
|
|
@@ -60,7 +60,7 @@ class AutocompleteTests(unittest.TestCase):
|
|
|
60
60
|
)
|
|
61
61
|
|
|
62
62
|
def test_payload_can_limit_area_types_for_vague_area_searches(self) -> None:
|
|
63
|
-
payload =
|
|
63
|
+
payload = LocationAutocomplete(
|
|
64
64
|
"amsterdam west",
|
|
65
65
|
area_types=("city", "municipality", "neighborhood", "wijk"),
|
|
66
66
|
size=5,
|
|
@@ -74,9 +74,9 @@ class AutocompleteTests(unittest.TestCase):
|
|
|
74
74
|
|
|
75
75
|
def test_invalid_payload_values_fail_before_network(self) -> None:
|
|
76
76
|
with self.assertRaises(ValueError):
|
|
77
|
-
|
|
77
|
+
LocationAutocomplete("").to_payload()
|
|
78
78
|
with self.assertRaises(ValueError):
|
|
79
|
-
|
|
79
|
+
LocationAutocomplete("amsterdam", size=0).to_payload()
|
|
80
80
|
|
|
81
81
|
def test_parse_location_suggestions_maps_geo_hits(self) -> None:
|
|
82
82
|
suggestions = parse_location_suggestions(AUTOCOMPLETE_RESPONSE)
|
|
@@ -2,6 +2,8 @@ import unittest
|
|
|
2
2
|
from unittest.mock import patch
|
|
3
3
|
|
|
4
4
|
from funda import Funda, FundaRequestError, Listing, LocationSuggestion
|
|
5
|
+
from funda.constants import LOCATION_AUTOCOMPLETE_TEMPLATE_ID, SEARCH_TEMPLATE_ID
|
|
6
|
+
from funda.exceptions import SearchError
|
|
5
7
|
|
|
6
8
|
|
|
7
9
|
class FakeResponse:
|
|
@@ -83,7 +85,27 @@ class FundaClientTests(unittest.TestCase):
|
|
|
83
85
|
url, payload, json_data = client._transport.posts[0]
|
|
84
86
|
self.assertIn("_msearch/template", url)
|
|
85
87
|
self.assertIsNone(json_data)
|
|
86
|
-
self.assertIn(
|
|
88
|
+
self.assertIn(SEARCH_TEMPLATE_ID, payload)
|
|
89
|
+
|
|
90
|
+
def test_search_raises_on_embedded_msearch_error(self) -> None:
|
|
91
|
+
# _msearch/template answers 200 even when the query fails (e.g. a
|
|
92
|
+
# stale search template), embedding the error inside the body. This
|
|
93
|
+
# must surface as an error rather than being parsed as zero results.
|
|
94
|
+
error_body = {
|
|
95
|
+
"responses": [
|
|
96
|
+
{
|
|
97
|
+
"error": {
|
|
98
|
+
"reason": "No mapping found for [placement_type] in order to sort on",
|
|
99
|
+
},
|
|
100
|
+
"status": 400,
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
client = self.client(FakeResponse(200, error_body))
|
|
105
|
+
|
|
106
|
+
with self.assertRaises(SearchError) as ctx:
|
|
107
|
+
client.search("amsterdam")
|
|
108
|
+
self.assertIn("placement_type", str(ctx.exception))
|
|
87
109
|
|
|
88
110
|
def test_autocomplete_posts_json_payload_and_parses_results(self) -> None:
|
|
89
111
|
client = self.client(FakeResponse(200, {"hits": {"hits": []}}))
|
|
@@ -100,7 +122,7 @@ class FundaClientTests(unittest.TestCase):
|
|
|
100
122
|
url, payload, json_data = client._transport.posts[0]
|
|
101
123
|
self.assertIn("geo-wonen-alias-prod/_search/template", url)
|
|
102
124
|
self.assertIsNone(payload)
|
|
103
|
-
self.assertEqual(json_data["id"],
|
|
125
|
+
self.assertEqual(json_data["id"], LOCATION_AUTOCOMPLETE_TEMPLATE_ID)
|
|
104
126
|
self.assertEqual(json_data["params"]["value"], "amsterdam west")
|
|
105
127
|
self.assertEqual(json_data["params"]["size"], 5)
|
|
106
128
|
self.assertEqual(
|
|
@@ -2,10 +2,30 @@ import os
|
|
|
2
2
|
import unittest
|
|
3
3
|
|
|
4
4
|
from funda import Funda
|
|
5
|
+
from funda.constants import SEARCH_TEMPLATE_ID
|
|
6
|
+
from funda.exceptions import SearchError
|
|
5
7
|
|
|
6
8
|
|
|
7
9
|
@unittest.skipUnless(os.environ.get("PYFUNDA_LIVE") == "1", "set PYFUNDA_LIVE=1")
|
|
8
10
|
class LiveFundaSmokeTests(unittest.TestCase):
|
|
11
|
+
def test_search_template_id_is_accepted_by_funda(self) -> None:
|
|
12
|
+
"""Guard against Funda rotating the stored search template ID.
|
|
13
|
+
|
|
14
|
+
A stale SEARCH_TEMPLATE_ID makes every search fail server-side with an
|
|
15
|
+
HTTP 400, while the mocked unit tests keep passing. This isolates that
|
|
16
|
+
failure mode with a clear pointer to the constant that needs updating.
|
|
17
|
+
"""
|
|
18
|
+
with Funda(timeout=30) as client:
|
|
19
|
+
try:
|
|
20
|
+
results = client.search("amsterdam")
|
|
21
|
+
except SearchError as exc:
|
|
22
|
+
self.fail(
|
|
23
|
+
f"Funda rejected search template {SEARCH_TEMPLATE_ID!r}: {exc}. "
|
|
24
|
+
"Update SEARCH_TEMPLATE_ID in funda/constants.py to the value "
|
|
25
|
+
"used by the current Funda app."
|
|
26
|
+
)
|
|
27
|
+
self.assertTrue(results, "search returned no listings for 'amsterdam'")
|
|
28
|
+
|
|
9
29
|
def test_core_listing_search_and_parallel_paths(self) -> None:
|
|
10
30
|
with Funda(timeout=30) as client:
|
|
11
31
|
listing = client.listing(43117443)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import unittest
|
|
3
3
|
|
|
4
|
+
from funda.constants import SEARCH_INDEX, SEARCH_TEMPLATE_ID
|
|
4
5
|
from funda.search import _Search
|
|
5
6
|
|
|
6
7
|
|
|
@@ -65,8 +66,8 @@ class SearchTests(unittest.TestCase):
|
|
|
65
66
|
lines = payload.splitlines()
|
|
66
67
|
|
|
67
68
|
self.assertEqual(len(lines), 2)
|
|
68
|
-
self.
|
|
69
|
-
self.
|
|
69
|
+
self.assertEqual(json.loads(lines[0])["index"], SEARCH_INDEX)
|
|
70
|
+
self.assertEqual(json.loads(lines[1])["id"], SEARCH_TEMPLATE_ID)
|
|
70
71
|
|
|
71
72
|
|
|
72
73
|
if __name__ == "__main__":
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|