pypararius 2.0.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.
@@ -0,0 +1,26 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.so
5
+ .Python
6
+ build/
7
+ develop-eggs/
8
+ dist/
9
+ downloads/
10
+ eggs/
11
+ .eggs/
12
+ lib/
13
+ lib64/
14
+ parts/
15
+ sdist/
16
+ var/
17
+ wheels/
18
+ *.egg-info/
19
+ .installed.cfg
20
+ *.egg
21
+ .env
22
+ .venv
23
+ env/
24
+ venv/
25
+ .uv/
26
+ .python-version
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: pypararius
3
+ Version: 2.0.0
4
+ Summary: Python API wrapper for Pararius.com real estate listings
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: httpx>=0.27.0
7
+ Provides-Extra: dev
8
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
9
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
@@ -0,0 +1,314 @@
1
+ # pypararius
2
+
3
+ Python API for Pararius.com - the Netherlands' largest rental property platform.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install pypararius
9
+ # or with uv
10
+ uv add pypararius
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```python
16
+ from pypararius import Pararius
17
+
18
+ p = Pararius()
19
+
20
+ # Search for listings
21
+ results = p.search_listing('amsterdam', price_max=2000)
22
+ for r in results[:3]:
23
+ print(r['title'], r['price'])
24
+
25
+ # Get full listing details
26
+ listing = p.get_listing(results[0]['url'])
27
+ print(listing['title'], listing['city'])
28
+ print(listing.summary())
29
+ ```
30
+
31
+ ## How It Works
32
+
33
+ This library uses Pararius.com's internal endpoints. Pararius only has a website, so we use their frontend's AJAX endpoints.
34
+
35
+ ### Discovery Process
36
+
37
+ The API was discovered by analyzing network traffic from the Pararius website:
38
+
39
+ 1. Observed that search pages make XHR requests that return JSON instead of HTML
40
+ 2. Identified the `X-Requested-With: XMLHttpRequest` header as the trigger for JSON responses
41
+ 3. Mapped URL path segments to filter parameters
42
+ 4. Found JSON-LD structured data embedded in listing detail pages
43
+
44
+ ### API Architecture
45
+
46
+ | Endpoint | Method | Purpose |
47
+ |----------|--------|---------|
48
+ | `/apartments/{city}/{filters}` | GET | Search listings (returns JSON with XHR header) |
49
+ | `/apartment-for-rent/{city}/{id}/{street}` | GET | Listing details (HTML with JSON-LD) |
50
+ | `/api/suggest` | GET | Location autocomplete |
51
+
52
+ ### Search Response
53
+
54
+ When the search endpoint receives the XHR header, it returns:
55
+
56
+ ```json
57
+ {
58
+ "components": {
59
+ "results": "<html>...</html>"
60
+ },
61
+ "search_query": {
62
+ "filters": {...},
63
+ "view_options": {...}
64
+ },
65
+ "_meta": {
66
+ "canonical_url": "..."
67
+ }
68
+ }
69
+ ```
70
+
71
+ The `components.results` contains pre-rendered HTML listing cards that are parsed to extract listing data.
72
+
73
+ ### Listing Details
74
+
75
+ Detail pages return full HTML with embedded JSON-LD:
76
+
77
+ ```html
78
+ <script type="application/ld+json">
79
+ {
80
+ "@type": "Apartment",
81
+ "name": "Apartment Ridderspoorweg",
82
+ "address": {...},
83
+ "geo": {"latitude": 52.xxx, "longitude": 4.xxx},
84
+ "floorSize": {"value": 75},
85
+ "offers": {"price": 1850}
86
+ }
87
+ </script>
88
+ ```
89
+
90
+ Additional data (bedrooms, deposit, energy rating, etc.) is extracted by parsing the HTML feature tables.
91
+
92
+ ### Authentication
93
+
94
+ No authentication required. All listing data is publicly accessible.
95
+
96
+
97
+ ## API Reference
98
+
99
+ ### Pararius
100
+
101
+ Main entry point for the API.
102
+
103
+ ```python
104
+ from pypararius import Pararius
105
+
106
+ p = Pararius(timeout=30)
107
+ ```
108
+
109
+ #### search_listing(location, ...)
110
+
111
+ Search for rental listings with filters.
112
+
113
+ ```python
114
+ results = p.search_listing(
115
+ location='amsterdam', # City name
116
+ price_min=1000, # Minimum rent
117
+ price_max=2000, # Maximum rent
118
+ area_min=50, # Minimum living area (m²)
119
+ bedrooms=2, # Minimum bedrooms
120
+ interior='furnished', # 'furnished', 'upholstered', 'shell'
121
+ sort='newest', # Sort order (see below)
122
+ page=0, # Page number (0-indexed)
123
+ )
124
+ ```
125
+
126
+ **Sort options:**
127
+
128
+ | Sort Value | Description |
129
+ |------------|-------------|
130
+ | `newest` | Most recently published first |
131
+ | `price_asc` | Lowest price first |
132
+ | `price_desc` | Highest price first |
133
+ | `area_asc` | Smallest area first |
134
+ | `area_desc` | Largest area first |
135
+
136
+ #### get_listing(url)
137
+
138
+ Get full details for a listing by URL.
139
+
140
+ ```python
141
+ # By full URL
142
+ listing = p.get_listing('https://www.pararius.com/apartment-for-rent/amsterdam/abc123/street')
143
+
144
+ # By partial path
145
+ listing = p.get_listing('amsterdam/abc123/street')
146
+ ```
147
+
148
+ ### Listing
149
+
150
+ Listing objects support dict-like access with convenient aliases.
151
+
152
+ **Basic info:**
153
+
154
+ ```python
155
+ listing['title'] # Property title/address
156
+ listing['city'] # City name
157
+ listing['postcode'] # Postal code
158
+ listing['neighbourhood'] # Neighbourhood name
159
+ ```
160
+
161
+ **Price:**
162
+
163
+ ```python
164
+ listing['price'] # Numeric price (monthly rent)
165
+ listing['price_formatted'] # Formatted price string
166
+ listing['deposit'] # Deposit amount
167
+ ```
168
+
169
+ **Property details:**
170
+
171
+ ```python
172
+ listing['living_area'] # Living area in m²
173
+ listing['rooms'] # Total number of rooms
174
+ listing['bedrooms'] # Number of bedrooms
175
+ listing['interior'] # Interior type (furnished, etc.)
176
+ listing['energy_label'] # Energy rating
177
+ listing['description'] # Full description text
178
+ ```
179
+
180
+ **Availability:**
181
+
182
+ ```python
183
+ listing['available'] # Available date
184
+ listing['offered_since'] # When listed
185
+ listing['rental_agreement'] # Contract type
186
+ ```
187
+
188
+ **Location:**
189
+
190
+ ```python
191
+ listing['coordinates'] # (lat, lng) tuple
192
+ listing['latitude'] # Latitude
193
+ listing['longitude'] # Longitude
194
+ ```
195
+
196
+ **Media:**
197
+
198
+ ```python
199
+ listing['photos'] # List of photo URLs
200
+ listing['photo_urls'] # Same as photos
201
+ listing['photo_count'] # Number of photos
202
+ ```
203
+
204
+ **Rules:**
205
+
206
+ ```python
207
+ listing['smoking_allowed'] # Boolean
208
+ listing['pets_allowed'] # Boolean
209
+ ```
210
+
211
+ **Broker:**
212
+
213
+ ```python
214
+ listing['broker'] # Agent name
215
+ listing['broker_url'] # Agent page URL
216
+ listing['broker_phone'] # Agent phone
217
+ ```
218
+
219
+ **Metadata:**
220
+
221
+ ```python
222
+ listing['url'] # Full Pararius URL
223
+ listing['characteristics'] # Dict of all features
224
+ ```
225
+
226
+ **Key aliases** - these all work:
227
+
228
+ | Alias | Canonical Key |
229
+ |-------|---------------|
230
+ | `name`, `address`, `street` | `title` |
231
+ | `location`, `locality` | `city` |
232
+ | `area`, `size`, `area_m2` | `living_area` |
233
+ | `images`, `pictures`, `media` | `photos` |
234
+ | `agent`, `realtor`, `makelaar` | `broker` |
235
+ | `zip`, `zipcode`, `postal_code` | `postcode` |
236
+ | `energy_rating` | `energy_label` |
237
+
238
+ #### Methods
239
+
240
+ ```python
241
+ listing.summary() # Text summary of the listing
242
+ listing.to_dict() # Convert to plain dictionary
243
+ listing.keys() # List available keys
244
+ listing.get('key') # Get with default (like dict.get)
245
+ listing.id # Get listing ID
246
+ ```
247
+
248
+ ## Examples
249
+
250
+ ### Find apartments in Amsterdam under €2000/month
251
+
252
+ ```python
253
+ from pypararius import Pararius
254
+
255
+ p = Pararius()
256
+ results = p.search_listing('amsterdam', price_max=2000)
257
+
258
+ for listing in results:
259
+ print(f"{listing['title']}")
260
+ print(f" Price: €{listing['price']}/month")
261
+ print(f" Area: {listing.get('living_area', 'N/A')} m²")
262
+ print(f" Bedrooms: {listing.get('bedrooms', 'N/A')}")
263
+ print()
264
+ ```
265
+
266
+ ### Get detailed listing information
267
+
268
+ ```python
269
+ from pypararius import Pararius
270
+
271
+ p = Pararius()
272
+ results = p.search_listing('rotterdam', price_max=1500)
273
+
274
+ if results:
275
+ listing = p.get_listing(results[0]['url'])
276
+ print(listing.summary())
277
+
278
+ # Access all characteristics
279
+ for key, value in listing['characteristics'].items():
280
+ print(f"{key}: {value}")
281
+ ```
282
+
283
+ ### Search with filters
284
+
285
+ ```python
286
+ from pypararius import Pararius
287
+
288
+ p = Pararius()
289
+ results = p.search_listing(
290
+ 'amsterdam',
291
+ price_min=1500,
292
+ price_max=2500,
293
+ bedrooms=2,
294
+ interior='furnished',
295
+ sort='price_asc',
296
+ )
297
+
298
+ print(f"Found {len(results)} listings")
299
+ ```
300
+
301
+ ### Using context manager
302
+
303
+ ```python
304
+ from pypararius import Pararius
305
+
306
+ with Pararius() as p:
307
+ results = p.search_listing('utrecht', price_max=1800)
308
+ for r in results[:5]:
309
+ print(f"{r['title']}: €{r['price']}")
310
+ ```
311
+
312
+ ## License
313
+
314
+ AGPL-3.0
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env python3
2
+ """Export Pararius search results to CSV or Excel.
3
+
4
+ Usage:
5
+ # Export to CSV
6
+ uv run examples/export_to_csv.py --location amsterdam --output listings.csv
7
+
8
+ # Export to Excel
9
+ uv run examples/export_to_csv.py --location amsterdam --output listings.xlsx
10
+
11
+ # With filters
12
+ uv run examples/export_to_csv.py -l amsterdam --max-price 2000 --min-area 50 -o results.csv
13
+
14
+ # Multiple pages
15
+ uv run examples/export_to_csv.py -l amsterdam --pages 3 -o all_listings.csv
16
+ """
17
+
18
+ import argparse
19
+ import csv
20
+ from pathlib import Path
21
+
22
+ from pypararius import Pararius
23
+
24
+ COLUMNS = [
25
+ "title",
26
+ "city",
27
+ "postcode",
28
+ "neighbourhood",
29
+ "price",
30
+ "living_area",
31
+ "bedrooms",
32
+ "rooms",
33
+ "energy_label",
34
+ "interior",
35
+ "url",
36
+ "latitude",
37
+ "longitude",
38
+ ]
39
+
40
+
41
+ def export_csv(listings: list[dict], output: Path) -> None:
42
+ with output.open("w", newline="", encoding="utf-8") as f:
43
+ writer = csv.DictWriter(f, fieldnames=COLUMNS)
44
+ writer.writeheader()
45
+ for listing in listings:
46
+ row = {col: listing.get(col, "") for col in COLUMNS}
47
+ writer.writerow(row)
48
+
49
+
50
+ def export_excel(listings: list[dict], output: Path) -> None:
51
+ try:
52
+ import openpyxl
53
+ except ImportError:
54
+ print("Excel export requires openpyxl: uv pip install openpyxl")
55
+ raise SystemExit(1)
56
+
57
+ wb = openpyxl.Workbook()
58
+ ws = wb.active
59
+ ws.title = "Pararius Listings"
60
+
61
+ # Header
62
+ ws.append(COLUMNS)
63
+
64
+ # Data
65
+ for listing in listings:
66
+ row = [listing.get(col, "") for col in COLUMNS]
67
+ ws.append(row)
68
+
69
+ # Auto-width columns
70
+ for col in ws.columns:
71
+ max_len = max(len(str(cell.value or "")) for cell in col)
72
+ ws.column_dimensions[col[0].column_letter].width = min(max_len + 2, 50)
73
+
74
+ wb.save(output)
75
+
76
+
77
+ def main():
78
+ parser = argparse.ArgumentParser(description="Export Pararius listings to CSV/Excel")
79
+ parser.add_argument("--location", "-l", required=True, help="City")
80
+ parser.add_argument("--output", "-o", required=True, help="Output file (.csv or .xlsx)")
81
+ parser.add_argument("--max-price", type=int, help="Maximum price")
82
+ parser.add_argument("--min-price", type=int, help="Minimum price")
83
+ parser.add_argument("--min-area", type=int, help="Minimum living area (m²)")
84
+ parser.add_argument("--bedrooms", type=int, help="Minimum bedrooms")
85
+ parser.add_argument("--pages", type=int, default=1, help="Number of pages to fetch")
86
+ args = parser.parse_args()
87
+
88
+ output = Path(args.output)
89
+ if output.suffix not in (".csv", ".xlsx"):
90
+ print("Output must be .csv or .xlsx")
91
+ raise SystemExit(1)
92
+
93
+ all_listings = []
94
+
95
+ with Pararius() as p:
96
+ for page in range(args.pages):
97
+ print(f"Fetching page {page + 1}...")
98
+ results = p.search_listing(
99
+ location=args.location,
100
+ price_min=args.min_price,
101
+ price_max=args.max_price,
102
+ area_min=args.min_area,
103
+ bedrooms=args.bedrooms,
104
+ page=page,
105
+ )
106
+ if not results:
107
+ break
108
+ all_listings.extend([r.to_dict() for r in results])
109
+
110
+ if not all_listings:
111
+ print("No listings found")
112
+ raise SystemExit(1)
113
+
114
+ print(f"Exporting {len(all_listings)} listings...")
115
+
116
+ if output.suffix == ".csv":
117
+ export_csv(all_listings, output)
118
+ else:
119
+ export_excel(all_listings, output)
120
+
121
+ print(f"Saved to {output}")
122
+
123
+
124
+ if __name__ == "__main__":
125
+ main()
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env python3
2
+ """Alert on new Pararius listings matching your search criteria.
3
+
4
+ Monitors a search and notifies when new listings appear.
5
+ Can send notifications via terminal, macOS notifications, or webhook.
6
+
7
+ Usage:
8
+ uv run examples/new_listings_alert.py --location amsterdam --max-price 2000
9
+
10
+ # With desktop notifications (macOS):
11
+ uv run examples/new_listings_alert.py --location amsterdam --notify
12
+
13
+ # With webhook (Discord, Slack, etc.):
14
+ uv run examples/new_listings_alert.py --location amsterdam --webhook "https://..."
15
+ """
16
+
17
+ import argparse
18
+ import json
19
+ import subprocess
20
+ from datetime import datetime
21
+ from pathlib import Path
22
+
23
+ import httpx
24
+
25
+ from pypararius import Pararius
26
+
27
+ SEEN_FILE = Path("seen_listings.json")
28
+
29
+
30
+ def load_seen() -> set:
31
+ if SEEN_FILE.exists():
32
+ return set(json.loads(SEEN_FILE.read_text()))
33
+ return set()
34
+
35
+
36
+ def save_seen(seen: set) -> None:
37
+ SEEN_FILE.write_text(json.dumps(list(seen)))
38
+
39
+
40
+ def notify_macos(title: str, message: str) -> None:
41
+ """Send macOS notification."""
42
+ subprocess.run([
43
+ "osascript", "-e",
44
+ f'display notification "{message}" with title "{title}"'
45
+ ], check=False)
46
+
47
+
48
+ def notify_webhook(webhook_url: str, listings: list) -> None:
49
+ """Send listings to a webhook (Discord/Slack compatible)."""
50
+ content = "\n".join([
51
+ f"**{l['title']}** ({l['city']}) - €{l['price']}/month\n{l['url']}"
52
+ for l in listings
53
+ ])
54
+ httpx.post(webhook_url, json={"content": content}, timeout=10)
55
+
56
+
57
+ def main():
58
+ parser = argparse.ArgumentParser(description="Alert on new Pararius listings")
59
+ parser.add_argument("--location", "-l", required=True, help="City")
60
+ parser.add_argument("--max-price", type=int, help="Maximum price")
61
+ parser.add_argument("--min-price", type=int, help="Minimum price")
62
+ parser.add_argument("--min-area", type=int, help="Minimum living area (m²)")
63
+ parser.add_argument("--bedrooms", type=int, help="Minimum bedrooms")
64
+ parser.add_argument("--notify", action="store_true", help="Send macOS notification")
65
+ parser.add_argument("--webhook", help="Webhook URL for notifications")
66
+ args = parser.parse_args()
67
+
68
+ seen = load_seen()
69
+ new_listings = []
70
+
71
+ with Pararius() as p:
72
+ results = p.search_listing(
73
+ location=args.location,
74
+ price_min=args.min_price,
75
+ price_max=args.max_price,
76
+ area_min=args.min_area,
77
+ bedrooms=args.bedrooms,
78
+ sort="newest",
79
+ )
80
+
81
+ for listing in results:
82
+ lid = str(listing.id)
83
+ if lid not in seen:
84
+ seen.add(lid)
85
+ new_listings.append({
86
+ "title": listing["title"],
87
+ "city": listing["city"],
88
+ "price": listing["price"],
89
+ "url": listing["url"],
90
+ "area": listing.get("living_area"),
91
+ })
92
+
93
+ save_seen(seen)
94
+
95
+ if not new_listings:
96
+ print(f"[{datetime.now():%H:%M}] No new listings")
97
+ return
98
+
99
+ print(f"[{datetime.now():%H:%M}] Found {len(new_listings)} new listing(s):\n")
100
+ for l in new_listings:
101
+ area = f", {l['area']}m²" if l.get("area") else ""
102
+ print(f" {l['title']} ({l['city']}{area})")
103
+ print(f" €{l['price']}/month")
104
+ print(f" {l['url']}\n")
105
+
106
+ if args.notify:
107
+ notify_macos(
108
+ "New Pararius Listings",
109
+ f"{len(new_listings)} new listing(s) in {args.location}"
110
+ )
111
+
112
+ if args.webhook:
113
+ notify_webhook(args.webhook, new_listings)
114
+
115
+
116
+ if __name__ == "__main__":
117
+ main()