pyfunda 2.3.0__tar.gz → 2.4.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.
- {pyfunda-2.3.0 → pyfunda-2.4.0}/PKG-INFO +1 -1
- pyfunda-2.4.0/examples/search_sold.py +90 -0
- {pyfunda-2.3.0 → pyfunda-2.4.0}/funda/funda.py +15 -1
- {pyfunda-2.3.0 → pyfunda-2.4.0}/pyproject.toml +1 -1
- {pyfunda-2.3.0 → pyfunda-2.4.0}/.github/FUNDING.yml +0 -0
- {pyfunda-2.3.0 → pyfunda-2.4.0}/.github/workflows/publish.yml +0 -0
- {pyfunda-2.3.0 → pyfunda-2.4.0}/.gitignore +0 -0
- {pyfunda-2.3.0 → pyfunda-2.4.0}/LICENSE +0 -0
- {pyfunda-2.3.0 → pyfunda-2.4.0}/README.md +0 -0
- {pyfunda-2.3.0 → pyfunda-2.4.0}/examples/analysis.ipynb +0 -0
- {pyfunda-2.3.0 → pyfunda-2.4.0}/examples/export_to_csv.py +0 -0
- {pyfunda-2.3.0 → pyfunda-2.4.0}/examples/new_listings_alert.py +0 -0
- {pyfunda-2.3.0 → pyfunda-2.4.0}/examples/poll_new_listings.py +0 -0
- {pyfunda-2.3.0 → pyfunda-2.4.0}/examples/price_history.py +0 -0
- {pyfunda-2.3.0 → pyfunda-2.4.0}/examples/price_tracker.py +0 -0
- {pyfunda-2.3.0 → pyfunda-2.4.0}/funda/__init__.py +0 -0
- {pyfunda-2.3.0 → pyfunda-2.4.0}/funda/listing.py +0 -0
- {pyfunda-2.3.0 → pyfunda-2.4.0}/funda/py.typed +0 -0
- {pyfunda-2.3.0 → pyfunda-2.4.0}/test_all_flows.py +0 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Search for sold listings on Funda.
|
|
3
|
+
|
|
4
|
+
Find recently sold properties in a given location to analyze
|
|
5
|
+
market prices and trends.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
uv run examples/search_sold.py amsterdam
|
|
9
|
+
uv run examples/search_sold.py rotterdam --max-price 500000
|
|
10
|
+
uv run examples/search_sold.py utrecht --pages 3
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
|
|
15
|
+
from funda import Funda
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main():
|
|
19
|
+
parser = argparse.ArgumentParser(description="Search for sold listings")
|
|
20
|
+
parser.add_argument("location", help="City or area to search in")
|
|
21
|
+
parser.add_argument("--min-price", type=int, help="Minimum price")
|
|
22
|
+
parser.add_argument("--max-price", type=int, help="Maximum price")
|
|
23
|
+
parser.add_argument("--pages", type=int, default=1, help="Number of pages (15 results each)")
|
|
24
|
+
args = parser.parse_args()
|
|
25
|
+
|
|
26
|
+
with Funda() as f:
|
|
27
|
+
print(f"Searching for sold listings in {args.location}...")
|
|
28
|
+
print()
|
|
29
|
+
|
|
30
|
+
all_listings = []
|
|
31
|
+
for page in range(args.pages):
|
|
32
|
+
results = f.search_listing(
|
|
33
|
+
location=args.location,
|
|
34
|
+
availability="sold",
|
|
35
|
+
price_min=args.min_price,
|
|
36
|
+
price_max=args.max_price,
|
|
37
|
+
sort="newest",
|
|
38
|
+
page=page,
|
|
39
|
+
)
|
|
40
|
+
all_listings.extend(results)
|
|
41
|
+
|
|
42
|
+
if len(results) < 15:
|
|
43
|
+
break
|
|
44
|
+
|
|
45
|
+
if not all_listings:
|
|
46
|
+
print("No sold listings found.")
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
print(f"Found {len(all_listings)} sold listings:")
|
|
50
|
+
print("-" * 70)
|
|
51
|
+
print(f"{'Address':<35} {'City':<15} {'Price':>12} {'m²':>6}")
|
|
52
|
+
print("-" * 70)
|
|
53
|
+
|
|
54
|
+
total_price = 0
|
|
55
|
+
total_area = 0
|
|
56
|
+
count_with_area = 0
|
|
57
|
+
|
|
58
|
+
for listing in all_listings:
|
|
59
|
+
title = listing["title"][:34]
|
|
60
|
+
city = (listing["city"] or "")[:14]
|
|
61
|
+
price = listing["price"]
|
|
62
|
+
area = listing["living_area"]
|
|
63
|
+
|
|
64
|
+
price_str = f"€{price:,}" if price else "N/A"
|
|
65
|
+
area_str = str(area) if area else "-"
|
|
66
|
+
|
|
67
|
+
print(f"{title:<35} {city:<15} {price_str:>12} {area_str:>6}")
|
|
68
|
+
|
|
69
|
+
if price:
|
|
70
|
+
total_price += price
|
|
71
|
+
if area:
|
|
72
|
+
total_area += area
|
|
73
|
+
count_with_area += 1
|
|
74
|
+
|
|
75
|
+
print("-" * 70)
|
|
76
|
+
|
|
77
|
+
# Summary statistics
|
|
78
|
+
if all_listings:
|
|
79
|
+
avg_price = total_price // len(all_listings)
|
|
80
|
+
print(f"\nAverage sold price: €{avg_price:,}")
|
|
81
|
+
|
|
82
|
+
if count_with_area > 0:
|
|
83
|
+
avg_area = total_area // count_with_area
|
|
84
|
+
avg_price_m2 = total_price // total_area
|
|
85
|
+
print(f"Average living area: {avg_area} m²")
|
|
86
|
+
print(f"Average price per m²: €{avg_price_m2:,}")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
main()
|
|
@@ -315,6 +315,7 @@ class Funda:
|
|
|
315
315
|
self,
|
|
316
316
|
location: str | list[str] | None = None,
|
|
317
317
|
offering_type: str = "buy",
|
|
318
|
+
availability: str | list[str] | None = None,
|
|
318
319
|
price_min: int | None = None,
|
|
319
320
|
price_max: int | None = None,
|
|
320
321
|
area_min: int | None = None,
|
|
@@ -332,6 +333,8 @@ class Funda:
|
|
|
332
333
|
Args:
|
|
333
334
|
location: City/area name(s) or postcode to search in
|
|
334
335
|
offering_type: "buy" or "rent"
|
|
336
|
+
availability: Filter by status - "available", "negotiations", "sold",
|
|
337
|
+
or a list combining them. Default: ["available", "negotiations"]
|
|
335
338
|
price_min: Minimum price
|
|
336
339
|
price_max: Maximum price
|
|
337
340
|
area_min: Minimum living area in m²
|
|
@@ -350,6 +353,7 @@ class Funda:
|
|
|
350
353
|
|
|
351
354
|
Example:
|
|
352
355
|
>>> f.search_listing('amsterdam', price_max=500000)
|
|
356
|
+
>>> f.search_listing('amsterdam', availability='sold') # sold listings
|
|
353
357
|
>>> f.search_listing('1012AB', radius_km=30, price_max=1250000, energy_label=['A', 'A+'])
|
|
354
358
|
"""
|
|
355
359
|
import json
|
|
@@ -359,9 +363,19 @@ class Funda:
|
|
|
359
363
|
if location:
|
|
360
364
|
locations = [location] if isinstance(location, str) else list(location)
|
|
361
365
|
|
|
366
|
+
# Normalize availability - map user-friendly "sold" to API's "unavailable"
|
|
367
|
+
if availability is None:
|
|
368
|
+
avail_list = ["available", "negotiations"]
|
|
369
|
+
elif isinstance(availability, str):
|
|
370
|
+
avail_list = [availability]
|
|
371
|
+
else:
|
|
372
|
+
avail_list = list(availability)
|
|
373
|
+
# Map "sold" to "unavailable" (API terminology)
|
|
374
|
+
avail_list = ["unavailable" if v == "sold" else v for v in avail_list]
|
|
375
|
+
|
|
362
376
|
# Build search params
|
|
363
377
|
params: dict[str, Any] = {
|
|
364
|
-
"availability":
|
|
378
|
+
"availability": avail_list,
|
|
365
379
|
"type": ["single"],
|
|
366
380
|
"zoning": ["residential"],
|
|
367
381
|
"object_type": object_type or ["house", "apartment"],
|
|
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
|