pyfunda 2.6.2__tar.gz → 2.7.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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyfunda
3
- Version: 2.6.2
3
+ Version: 2.7.0
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
@@ -307,6 +307,32 @@ for change in history:
307
307
 
308
308
  > **Note:** This fetches data from the Walter Living API. Only called when explicitly requested (lazy-loaded).
309
309
 
310
+ #### get_contact_info(listing)
311
+
312
+ Get the realtor/makelaar agency name and phone number for a listing. Accepts a `Listing`, a numeric id (globalId or tinyId), or a Funda URL.
313
+
314
+ ```python
315
+ listing = f.get_listing(43333315)
316
+ contact = f.get_contact_info(listing)
317
+
318
+ print(contact['name'], contact['phone'])
319
+ # Scheffer Makelaardij B.V. 020-2470322
320
+ ```
321
+
322
+ **Returns:** A dict with the primary broker hoisted to the top level:
323
+
324
+ | Field | Description |
325
+ |-------|-------------|
326
+ | `name` | Agency display name |
327
+ | `phone` | Agency phone number |
328
+ | `broker_id` | Numeric office id |
329
+ | `association` | Trade association code (e.g. `VN`) |
330
+ | `is_contacting_enabled` | Whether the in-app contact form is enabled |
331
+ | `listing_id` / `tiny_id` / `listing_status` | Listing meta |
332
+ | `brokers` | Full list of brokers (for the rare multi-agency case) |
333
+
334
+ Raises `LookupError` if the listing has no contact info exposed.
335
+
310
336
  ### Listing
311
337
 
312
338
  Listing objects support dict-like access with convenient aliases.
@@ -283,6 +283,32 @@ for change in history:
283
283
 
284
284
  > **Note:** This fetches data from the Walter Living API. Only called when explicitly requested (lazy-loaded).
285
285
 
286
+ #### get_contact_info(listing)
287
+
288
+ Get the realtor/makelaar agency name and phone number for a listing. Accepts a `Listing`, a numeric id (globalId or tinyId), or a Funda URL.
289
+
290
+ ```python
291
+ listing = f.get_listing(43333315)
292
+ contact = f.get_contact_info(listing)
293
+
294
+ print(contact['name'], contact['phone'])
295
+ # Scheffer Makelaardij B.V. 020-2470322
296
+ ```
297
+
298
+ **Returns:** A dict with the primary broker hoisted to the top level:
299
+
300
+ | Field | Description |
301
+ |-------|-------------|
302
+ | `name` | Agency display name |
303
+ | `phone` | Agency phone number |
304
+ | `broker_id` | Numeric office id |
305
+ | `association` | Trade association code (e.g. `VN`) |
306
+ | `is_contacting_enabled` | Whether the in-app contact form is enabled |
307
+ | `listing_id` / `tiny_id` / `listing_status` | Listing meta |
308
+ | `brokers` | Full list of brokers (for the rare multi-agency case) |
309
+
310
+ Raises `LookupError` if the listing has no contact info exposed.
311
+
286
312
  ### Listing
287
313
 
288
314
  Listing objects support dict-like access with convenient aliases.
@@ -16,6 +16,7 @@ API_BASE = "https://listing-detail-page.funda.io/api/v4/listing/object/nl"
16
16
  API_LISTING = f"{API_BASE}/{{listing_id}}"
17
17
  API_LISTING_TINY = f"{API_BASE}/tinyId/{{tiny_id}}"
18
18
  API_SEARCH = "https://listing-search-wonen.funda.io/_msearch/template"
19
+ API_CONTACTS = "https://contacts-flows-bff.funda.io/api/v1/contacts-flows/listings/{listing_id}/contact-block"
19
20
  API_WALTER = "https://api.walterliving.com/hunter/lookup"
20
21
 
21
22
  # Funda mobile app JA3 fingerprints (captured from real Dart/Flutter app traffic)
@@ -792,6 +793,108 @@ class Funda:
792
793
 
793
794
  return changes
794
795
 
796
+ def get_contact_info(self, listing: "Listing | int | str") -> dict:
797
+ """Get realtor/makelaar contact info for a listing.
798
+
799
+ Returns the agency name, phone number, and association code as exposed
800
+ by the Funda Android app's contact block endpoint.
801
+
802
+ Args:
803
+ listing: A Listing object, a numeric id (globalId or tinyId), or a
804
+ Funda URL. tinyIds and URLs are resolved to a globalId via
805
+ ``get_listing`` before the contact lookup.
806
+
807
+ Returns:
808
+ Dict with the primary broker hoisted to the top level for
809
+ ergonomics (``name``, ``phone``, ``broker_id``, ``association``)
810
+ plus listing meta (``listing_id``, ``tiny_id``, ``listing_status``)
811
+ and the full ``brokers`` list for the rare multi-agency case.
812
+
813
+ Raises:
814
+ LookupError: Listing exists but has no contact info (HTTP 204) or
815
+ the endpoint returned a non-200 status.
816
+
817
+ Example:
818
+ >>> listing = f.get_listing(43333315)
819
+ >>> contact = f.get_contact_info(listing)
820
+ >>> contact['name'], contact['phone']
821
+ ('Scheffer Makelaardij B.V.', '020-2470322')
822
+ """
823
+ # Resolve to global_id (the endpoint expects the numeric listingId,
824
+ # not the tinyId shown in funda.nl URLs).
825
+ global_id: int | None = None
826
+ if isinstance(listing, Listing):
827
+ global_id = listing.get("global_id")
828
+ elif isinstance(listing, str) and "funda.nl" in listing:
829
+ global_id = self.get_listing(listing).get("global_id")
830
+ else:
831
+ id_str = str(listing)
832
+ if not id_str.isdigit():
833
+ raise ValueError(f"Unrecognized listing identifier: {listing!r}")
834
+ # tinyIds are 8-9 digits; resolve them to a globalId via the
835
+ # listing detail endpoint. 7-digit ids are already globalIds.
836
+ if len(id_str) >= 8:
837
+ global_id = self.get_listing(int(id_str)).get("global_id")
838
+ else:
839
+ global_id = int(id_str)
840
+
841
+ if not global_id:
842
+ raise ValueError("Could not determine listing globalId")
843
+
844
+ url = API_CONTACTS.format(listing_id=global_id)
845
+ headers = _make_headers()
846
+ response = self._get(url, headers)
847
+
848
+ if response.status_code == 204:
849
+ raise LookupError(f"Listing {global_id} has no contact info")
850
+ if response.status_code != 200:
851
+ raise LookupError(
852
+ f"Could not fetch contact info (status {response.status_code})"
853
+ )
854
+
855
+ return self._parse_contact_info(response.json())
856
+
857
+ def _parse_contact_info(self, data: dict) -> dict:
858
+ """Normalize the contact-block payload into a flat dict."""
859
+ brokers = [
860
+ {
861
+ "broker_id": b.get("id"),
862
+ "name": b.get("displayName"),
863
+ "phone": b.get("phoneNumber"),
864
+ "association": b.get("associationCode"),
865
+ "logo_url": b.get("logoUrl") or None,
866
+ "banner_url": b.get("bannerUrl"),
867
+ "is_contacting_enabled": b.get("isContactingEnabled"),
868
+ "personal_contact_info": b.get("personalContactBlockInfo"),
869
+ }
870
+ for b in data.get("contactBlockDetails", [])
871
+ ]
872
+
873
+ result: dict[str, Any] = {
874
+ "listing_id": data.get("id"),
875
+ "tiny_id": data.get("tinyId"),
876
+ "listing_status": data.get("listingStatus"),
877
+ "is_viewing_planner_enabled": data.get("isViewingPlannerEnabled"),
878
+ "brokers": brokers,
879
+ }
880
+
881
+ # Hoist primary broker fields to the top level (single-agency is the
882
+ # overwhelming common case).
883
+ if brokers:
884
+ primary = brokers[0]
885
+ result.update({
886
+ "broker_id": primary["broker_id"],
887
+ "name": primary["name"],
888
+ "phone": primary["phone"],
889
+ "association": primary["association"],
890
+ "logo_url": primary["logo_url"],
891
+ "banner_url": primary["banner_url"],
892
+ "is_contacting_enabled": primary["is_contacting_enabled"],
893
+ "personal_contact_info": primary["personal_contact_info"],
894
+ })
895
+
896
+ return result
897
+
795
898
  def _parse_search_results(self, data: dict) -> list[Listing]:
796
899
  """Parse search API response into list of Listings."""
797
900
  listings = []
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pyfunda"
7
- version = "2.6.2"
7
+ version = "2.7.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"
@@ -757,6 +757,60 @@ def test_listing_setitem():
757
757
  print(" setitem working correctly")
758
758
 
759
759
 
760
+ # =============================================================================
761
+ # FLOW 18: Realtor Contact Info
762
+ # =============================================================================
763
+
764
+ @test("Get contact info by globalId")
765
+ def test_get_contact_info_by_global_id():
766
+ from funda import Funda
767
+ f = Funda()
768
+ contact = f.get_contact_info(7988952)
769
+
770
+ assert contact['name'], "Should have agency name"
771
+ assert contact['phone'], "Should have phone number"
772
+ assert contact['broker_id'], "Should have broker_id"
773
+ assert contact['listing_id'] == '7988952', "Should echo listing_id"
774
+ assert contact['tiny_id'] == '43333315', "Should include tiny_id"
775
+ assert isinstance(contact['brokers'], list) and contact['brokers'], "Should have brokers list"
776
+ print(f" {contact['name']} | {contact['phone']}")
777
+
778
+
779
+ @test("Get contact info by Listing object")
780
+ def test_get_contact_info_by_listing():
781
+ from funda import Funda
782
+ f = Funda()
783
+ listing = f.get_listing(43333315)
784
+ contact = f.get_contact_info(listing)
785
+
786
+ assert contact['phone'], "Should have phone number"
787
+ assert contact['name'], "Should have agency name"
788
+ print(f" {contact['name']} | {contact['phone']}")
789
+
790
+
791
+ @test("Get contact info by tinyId resolves to globalId")
792
+ def test_get_contact_info_by_tiny_id():
793
+ from funda import Funda
794
+ f = Funda()
795
+ contact = f.get_contact_info(43333315)
796
+
797
+ assert contact['listing_id'] == '7988952', "tinyId should resolve to globalId"
798
+ assert contact['phone'], "Should have phone number"
799
+
800
+
801
+ @test("Get contact info raises LookupError for unknown listing")
802
+ def test_get_contact_info_not_found():
803
+ from funda import Funda
804
+ f = Funda()
805
+
806
+ try:
807
+ f.get_contact_info(1)
808
+ assert False, "Should have raised LookupError"
809
+ except LookupError as e:
810
+ assert "no contact info" in str(e).lower() or "could not fetch" in str(e).lower()
811
+ print(f" Correctly raised: {e}")
812
+
813
+
760
814
  # =============================================================================
761
815
  # Run all tests
762
816
  # =============================================================================
@@ -849,6 +903,12 @@ def run_all_tests():
849
903
 
850
904
  # Flow 17: Listing Set Item
851
905
  test_listing_setitem,
906
+
907
+ # Flow 18: Realtor Contact Info
908
+ test_get_contact_info_by_global_id,
909
+ test_get_contact_info_by_listing,
910
+ test_get_contact_info_by_tiny_id,
911
+ test_get_contact_info_not_found,
852
912
  ]
853
913
 
854
914
  start_time = time.time()
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