pyhemnet 0.1.0__py3-none-any.whl

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.
pyhemnet/__init__.py ADDED
@@ -0,0 +1,15 @@
1
+ """Swedish Real Estate Scraper - Scrape data from Hemnet.se"""
2
+
3
+ __version__ = "0.1.0"
4
+ __author__ = "ningdp2012"
5
+
6
+ # Import scraper classes
7
+ from .hemnet import HemnetScraper
8
+
9
+ # Import enums
10
+ from .constants import HemnetItemType
11
+
12
+ __all__ = [
13
+ "HemnetScraper",
14
+ "HemnetItemType",
15
+ ]
pyhemnet/constants.py ADDED
@@ -0,0 +1,21 @@
1
+ """Constants for Hemnet scraper"""
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class HemnetItemType(str, Enum):
7
+ """Hemnet property item types"""
8
+ VILLA = "villa"
9
+ RADHUS = "radhus"
10
+ BOSTADSRATT = "bostadsratt"
11
+ FRITIDSHUS = "fritidshus"
12
+ TOMT = "tomt"
13
+ GARD = "gard"
14
+ OTHER = "other"
15
+
16
+
17
+ # Hemnet URLs mapping
18
+ HEMNET_URLS = {
19
+ "listings": "https://www.hemnet.se/bostader",
20
+ "sold": "https://www.hemnet.se/salda/bostader"
21
+ }
pyhemnet/hemnet.py ADDED
@@ -0,0 +1,376 @@
1
+ """Hemnet scraper module for property sales data extraction"""
2
+
3
+ import datetime as dt
4
+ import json
5
+ import logging
6
+ import re
7
+
8
+ import cloudscraper
9
+ from bs4 import BeautifulSoup
10
+
11
+ from .constants import HEMNET_URLS, HemnetItemType
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class HemnetScraper:
17
+ """Scraper for Hemnet property sales data
18
+
19
+ Examples:
20
+ >>> scraper = HemnetScraper()
21
+ >>> listing, sold = scraper.get_summary(location_id="17932")
22
+ >>> homes = scraper.get_sold(
23
+ ... location_id="17932",
24
+ ... item_types=[HemnetItemType.VILLA, HemnetItemType.RADHUS]
25
+ ... )
26
+
27
+ # With strings
28
+ >>> homes = scraper.get_sold(
29
+ ... location_id="17932",
30
+ ... item_types=["villa", "radhus"]
31
+ ... )
32
+ """
33
+
34
+ def __init__(self):
35
+ """Initialize the Hemnet scraper"""
36
+ self.scraper = cloudscraper.create_scraper()
37
+
38
+ def _build_url(
39
+ self,
40
+ url_type: str,
41
+ location_id: str | None = None,
42
+ item_types: list[HemnetItemType | str] | None = None
43
+ ) -> str:
44
+ """Build URL dynamically based on type (internal)
45
+
46
+ Args:
47
+ url_type: Type of URL to build ('listings' or 'sold')
48
+ location_id: Hemnet location ID (optional)
49
+ item_types: List of property types (optional)
50
+
51
+ Returns:
52
+ Constructed URL string
53
+
54
+ Raises:
55
+ ValueError: If url_type is invalid
56
+ """
57
+ # Select base URL
58
+ base_url = HEMNET_URLS.get(url_type)
59
+ if base_url is None:
60
+ raise ValueError(f"Invalid url_type: {url_type}. Must be 'listings' or 'sold'")
61
+
62
+ params = []
63
+
64
+ # Add location_id if provided
65
+ if location_id:
66
+ params.append(f"location_ids[]={location_id}")
67
+
68
+ # Add item_types if provided
69
+ if item_types:
70
+ item_type_strs = [t.value if isinstance(t, HemnetItemType) else t for t in item_types]
71
+ for item_type in item_type_strs:
72
+ params.append(f"item_types[]={item_type}")
73
+
74
+ # Add sorting parameters
75
+ if url_type == 'listings':
76
+ params.extend(["by=creation", "order=desc"])
77
+ else: # sold
78
+ params.extend(["by=sale_date", "order=desc"])
79
+
80
+ # Build final URL
81
+ if params:
82
+ return f"{base_url}?{'&'.join(params)}"
83
+ return base_url
84
+
85
+ def _make_request(self, url: str) -> dict:
86
+ """Fetch and parse JSON data from Hemnet page (internal)
87
+
88
+ Args:
89
+ url: URL to scrape
90
+
91
+ Returns:
92
+ Parsed JSON data from page
93
+
94
+ Raises:
95
+ ValueError: If JSON data cannot be found or parsed
96
+ """
97
+ response = self.scraper.get(url)
98
+ response.raise_for_status()
99
+
100
+ soup = BeautifulSoup(response.text, "html.parser")
101
+ script_tag = soup.find("script", id="__NEXT_DATA__")
102
+
103
+ if not script_tag or not script_tag.string:
104
+ raise ValueError(f"Could not find JSON data in page: {url}")
105
+
106
+ try:
107
+ return json.loads(script_tag.string)
108
+ except json.JSONDecodeError as e:
109
+ raise ValueError(f"Failed to parse JSON data from {url}: {e}")
110
+
111
+ @staticmethod
112
+ def _extract_int(value: str | None, default: int = 0) -> int:
113
+ """Extract integer from string by removing non-digits (internal helper)"""
114
+ if not value:
115
+ return default
116
+ cleaned = re.sub(r"[^\d]", "", str(value))
117
+ return int(cleaned) if cleaned else default
118
+
119
+ @staticmethod
120
+ def _extract_housing_type(data: dict) -> str | None:
121
+ """Extract housing type from housing form data (internal helper)"""
122
+ housing_form = data.get("housingForm")
123
+ return housing_form.get("name") if isinstance(housing_form, dict) else None
124
+
125
+ @staticmethod
126
+ def _extract_labels(data: dict) -> list[str]:
127
+ """Extract text labels from label data (internal helper)"""
128
+ return [
129
+ label["text"]
130
+ for label in data.get("labels", [])
131
+ if isinstance(label, dict) and "text" in label
132
+ ]
133
+
134
+ @staticmethod
135
+ def _format_timestamp(timestamp: int | None) -> str | None:
136
+ """Format Unix timestamp to YYYY-MM-DD string (internal helper)"""
137
+ if not timestamp:
138
+ return None
139
+ return dt.datetime.fromtimestamp(float(timestamp)).strftime("%Y-%m-%d")
140
+
141
+ @staticmethod
142
+ def _clean_area_string(value: str | None) -> str:
143
+ """Clean area string by replacing non-breaking spaces (internal helper)"""
144
+ return re.sub(r"\xa0", " ", str(value or "0"))
145
+
146
+ def _parse_summary(self, json_data: dict) -> tuple[int, int]:
147
+ """Extract total listings and sold homes from JSON data (internal)
148
+
149
+ Args:
150
+ json_data: Parsed JSON data from page
151
+
152
+ Returns:
153
+ Tuple of (total_listings, total_sold)
154
+
155
+ Raises:
156
+ ValueError: If required data is missing or invalid
157
+ """
158
+ try:
159
+ summary = json_data["props"]["pageProps"]["__APOLLO_STATE__"]["ROOT_QUERY"]
160
+
161
+ # Find listing data
162
+ listing_data = next(
163
+ (v for k, v in summary.items() if k.startswith("searchForSaleListings")),
164
+ None
165
+ )
166
+ if listing_data is None or "total" not in listing_data:
167
+ raise ValueError("Missing searchForSaleListings data")
168
+
169
+ # Find sold data
170
+ sold_data = next(
171
+ (v for k, v in summary.items() if k.startswith("searchSales")),
172
+ None
173
+ )
174
+ if sold_data is None or "total" not in sold_data:
175
+ raise ValueError("Missing searchSales data")
176
+
177
+ listing = int(listing_data["total"])
178
+ sold = int(sold_data["total"])
179
+
180
+ return listing, sold
181
+
182
+ except (KeyError, TypeError) as e:
183
+ raise ValueError(f"Invalid JSON structure: {e}")
184
+ except ValueError as e:
185
+ if "invalid literal" in str(e):
186
+ raise ValueError(f"Non-numeric total value: {e}")
187
+ raise
188
+
189
+ def _parse_listing_details(self, json_data: dict) -> list[dict]:
190
+ """Extract details of active listings from JSON data (internal)
191
+
192
+ Args:
193
+ json_data: Parsed JSON data from page
194
+
195
+ Returns:
196
+ List of dictionaries containing home details
197
+
198
+ Raises:
199
+ ValueError: If required data structure is missing or invalid
200
+ """
201
+ try:
202
+ details = json_data["props"]["pageProps"]["__APOLLO_STATE__"]
203
+ except (KeyError, TypeError) as e:
204
+ raise ValueError(f"Invalid JSON structure for details: {e}")
205
+
206
+ # Use list comprehension instead of dict comprehension + .values()
207
+ listing_cards = [
208
+ value for key, value in details.items()
209
+ if key.startswith("ListingCard:") and isinstance(value, dict) and value.get("__typename") == "ListingCard"
210
+ ]
211
+
212
+ homes = []
213
+ for data in listing_cards:
214
+ try:
215
+ homes.append({
216
+ "id": data.get("id"),
217
+ "address": data.get("streetAddress"),
218
+ "location": data.get("locationDescription"),
219
+ "housing_type": self._extract_housing_type(data),
220
+ "rooms": self._extract_int(data.get("rooms")),
221
+ "living_area": self._clean_area_string(data.get("livingAndSupplementalAreas")),
222
+ "land_area": self._clean_area_string(data.get("landArea")),
223
+ "asking_price": self._extract_int(data.get("askingPrice")),
224
+ "published_at": self._format_timestamp(data.get("publishedAt")),
225
+ "removed_before_showing": data.get("removedBeforeShowing"),
226
+ "new_construction": data.get("newConstruction"),
227
+ "broker_name": data.get("brokerName"),
228
+ "broker_agent": data.get("brokerAgencyName"),
229
+ "labels": self._extract_labels(data),
230
+ "description": data.get("description"),
231
+ })
232
+ except (ValueError, TypeError, AttributeError) as e:
233
+ # Skip individual listings with bad data rather than failing entire parse
234
+ logger.debug(f"Skipping listing {data.get('id', 'unknown')}: {e}")
235
+ continue
236
+
237
+ return homes
238
+
239
+ def _parse_sold_details(self, json_data: dict) -> list[dict]:
240
+ """Extract details of sold homes from JSON data (internal)
241
+
242
+ Args:
243
+ json_data: Parsed JSON data from page
244
+
245
+ Returns:
246
+ List of dictionaries containing home details
247
+
248
+ Raises:
249
+ ValueError: If required data structure is missing or invalid
250
+ """
251
+ try:
252
+ details = json_data["props"]["pageProps"]["__APOLLO_STATE__"]
253
+ except (KeyError, TypeError) as e:
254
+ raise ValueError(f"Invalid JSON structure for details: {e}")
255
+
256
+ # Use list comprehension instead of dict comprehension + .values()
257
+ sale_cards = [
258
+ value for key, value in details.items()
259
+ if key.startswith("SaleCard:") and isinstance(value, dict) and value.get("__typename") == "SaleCard"
260
+ ]
261
+
262
+ homes = []
263
+ for data in sale_cards:
264
+ try:
265
+ homes.append({
266
+ "id": data.get("id"),
267
+ "listing_id": data.get("listingId"),
268
+ "address": data.get("streetAddress"),
269
+ "location": data.get("locationDescription"),
270
+ "housing_type": self._extract_housing_type(data),
271
+ "rooms": self._extract_int(data.get("rooms")),
272
+ "living_area": self._clean_area_string(data.get("livingArea")),
273
+ "land_area": self._clean_area_string(data.get("landArea")),
274
+ "asking_price": self._extract_int(data.get("askingPrice")),
275
+ "final_price": self._extract_int(data.get("finalPrice")),
276
+ "price_change": self._clean_area_string(data.get("priceChange")),
277
+ "sold_at": self._format_timestamp(data.get("soldAt")),
278
+ "broker": data.get("brokerAgencyName"),
279
+ "labels": self._extract_labels(data),
280
+ })
281
+ except (ValueError, TypeError, AttributeError) as e:
282
+ # Skip individual listings with bad data rather than failing entire parse
283
+ logger.debug(f"Skipping sold home {data.get('id', 'unknown')}: {e}")
284
+ continue
285
+
286
+ return homes
287
+
288
+ def get_summary(
289
+ self,
290
+ location_id: str | None = None,
291
+ item_types: list[HemnetItemType | str] | None = None
292
+ ) -> tuple[int, int]:
293
+ """Get summary statistics for all properties
294
+
295
+ Args:
296
+ location_id: Hemnet location ID (optional)
297
+ item_types: List of property types (optional)
298
+
299
+ Returns:
300
+ Tuple of (total_listings, total_sold)
301
+
302
+ Raises:
303
+ ValueError: If the page structure is invalid or data cannot be parsed
304
+ requests.exceptions.HTTPError: If the HTTP request fails
305
+ """
306
+ try:
307
+ url = self._build_url('listings', location_id, item_types)
308
+ json_data = self._make_request(url)
309
+ listing, sold = self._parse_summary(json_data)
310
+ return listing, sold
311
+ except ValueError as e:
312
+ raise ValueError(f"Failed to get summary statistics: {e}")
313
+
314
+ def get_listings(
315
+ self,
316
+ location_id: str | None = None,
317
+ item_types: list[HemnetItemType | str] | None = None
318
+ ) -> list[dict]:
319
+ """Get detailed listings for properties currently for sale
320
+
321
+ Args:
322
+ location_id: Hemnet location ID (optional)
323
+ item_types: List of property types (optional)
324
+ Returns:
325
+ List of dictionaries containing property details
326
+ Raises:
327
+ ValueError: If the page structure is invalid or data cannot be parsed
328
+ requests.exceptions.HTTPError: If the HTTP request fails
329
+ """
330
+ try:
331
+ url = self._build_url('listings', location_id, item_types)
332
+ json_data = self._make_request(url)
333
+ listings = self._parse_listing_details(json_data)
334
+ return listings
335
+ except ValueError as e:
336
+ raise ValueError(f"Failed to get property listings: {e}")
337
+
338
+ def get_sold(
339
+ self,
340
+ location_id: str | None = None,
341
+ item_types: list[HemnetItemType | str] | None = None
342
+ ) -> list[dict]:
343
+ """Get detailed listings for sold homes
344
+
345
+ Args:
346
+ location_id: Hemnet location ID (optional)
347
+ item_types: List of property types (optional)
348
+
349
+ Returns:
350
+ List of dictionaries containing sold home details, where each dict has:
351
+ - id: Hemnet ID
352
+ - listing_id: Listing identifier
353
+ - address: Street address
354
+ - location: Location description
355
+ - housing_type: Type of housing (villa, apartment, etc.)
356
+ - rooms: Number of rooms
357
+ - living_area: Living area with units
358
+ - land_area: Land area with units
359
+ - asking_price: Initial asking price (int)
360
+ - final_price: Final sold price (int)
361
+ - price_change: Price change information
362
+ - sold_at: Sale date (YYYY-MM-DD format or None)
363
+ - broker: Broker agency name
364
+ - labels: List of property labels/tags
365
+
366
+ Raises:
367
+ ValueError: If the page structure is invalid or data cannot be parsed
368
+ requests.exceptions.HTTPError: If the HTTP request fails
369
+ """
370
+ try:
371
+ url = self._build_url('sold', location_id, item_types)
372
+ json_data = self._make_request(url)
373
+ homes = self._parse_sold_details(json_data)
374
+ return homes
375
+ except ValueError as e:
376
+ raise ValueError(f"Failed to get sold homes listings: {e}")
@@ -0,0 +1,246 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyhemnet
3
+ Version: 0.1.0
4
+ Summary: A Python library for accessing Hemnet.se property data
5
+ Author-email: ningdp2012 <ningdp2012@example.com>
6
+ License: MIT
7
+ Project-URL: Repository, https://github.com/ningdp2012/pyhemnet
8
+ Keywords: hemnet,real estate,sweden,property,housing,data
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: cloudscraper>=1.2.71
20
+ Requires-Dist: beautifulsoup4>=4.12.0
21
+ Requires-Dist: requests>=2.31.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
24
+ Requires-Dist: black>=23.0.0; extra == "dev"
25
+ Requires-Dist: flake8>=6.0.0; extra == "dev"
26
+ Dynamic: license-file
27
+
28
+ # PyHemnet
29
+
30
+ A Python library for accessing Hemnet.se property data. Extract property sales information including prices, locations, sizes, and detailed property characteristics.
31
+
32
+ ## Features
33
+
34
+ - 🏠 **Hemnet Data Access**: Access property sales data from Hemnet.se
35
+ - Get summary statistics on properties for sale and sold properties
36
+ - Extract detailed information including prices, location, size, broker, and more
37
+ - Filter by location and property types
38
+ - Support for multiple property types (villa, radhus, bostadsrätt, etc.)
39
+
40
+ - 🚀 Easy-to-use Python API
41
+ - 💻 Object-oriented design with clean interfaces
42
+
43
+ ## Installation
44
+
45
+ Install from PyPI:
46
+
47
+ ```bash
48
+ pip install pyhemnet
49
+ ```
50
+
51
+ Or install from source:
52
+
53
+ ```bash
54
+ git clone https://github.com/ningdp2012/pyhemnet.git
55
+ cd pyhemnet
56
+ pip install -e .
57
+ ```
58
+
59
+ ## Quick Start
60
+
61
+ ```python
62
+ from pyhemnet import HemnetScraper, HemnetItemType
63
+
64
+ # Create a scraper instance
65
+ scraper = HemnetScraper()
66
+
67
+ # Get summary statistics
68
+ listing_count, sold_count = scraper.get_summary(location_id="17744")
69
+ print(f"Properties for sale: {listing_count}")
70
+ print(f"Sold properties: {sold_count}")
71
+
72
+ # Get detailed sold properties
73
+ homes = scraper.get_sold(
74
+ location_id="17744",
75
+ item_types=[HemnetItemType.VILLA, HemnetItemType.RADHUS]
76
+ )
77
+
78
+ for home in homes:
79
+ print(f"{home['address']} - {home['final_price']} SEK")
80
+ ```
81
+
82
+ ## Usage
83
+
84
+ ### Initialize the Scraper
85
+
86
+ ```python
87
+ from pyhemnet import HemnetScraper, HemnetItemType
88
+
89
+ scraper = HemnetScraper()
90
+ ```
91
+
92
+ ### Get Summary Statistics
93
+
94
+ Get counts of properties for sale and sold:
95
+
96
+ ```python
97
+ # Get summary for a specific location
98
+ listing_count, sold_count = scraper.get_summary(location_id="17744")
99
+ print(f"For sale: {listing_count}, Sold: {sold_count}")
100
+
101
+ # Filter by property types
102
+ listing_count, sold_count = scraper.get_summary(
103
+ location_id="17744",
104
+ item_types=[HemnetItemType.VILLA]
105
+ )
106
+ ```
107
+
108
+ ### Get Sold Properties
109
+
110
+ Retrieve detailed information about sold properties:
111
+
112
+ ```python
113
+ homes = scraper.get_sold(
114
+ location_id="17744",
115
+ item_types=[HemnetItemType.VILLA, HemnetItemType.RADHUS]
116
+ )
117
+
118
+ for home in homes:
119
+ print(f"Address: {home['address']}")
120
+ print(f"Final price: {home['final_price']} SEK")
121
+ print(f"Asking price: {home['asking_price']} SEK")
122
+ print(f"Living area: {home['living_area']}")
123
+ print(f"Sold date: {home['sold_at']}")
124
+ print("---")
125
+ ```
126
+
127
+ ### Get Current Listings
128
+
129
+ Get properties currently for sale:
130
+
131
+ ```python
132
+ listings = scraper.get_listings(
133
+ location_id="17744",
134
+ item_types=[HemnetItemType.BOSTADSRATT]
135
+ )
136
+
137
+ for listing in listings:
138
+ print(f"Address: {listing['address']}")
139
+ print(f"Price: {listing['asking_price']} SEK")
140
+ print(f"Published: {listing['published_at']}")
141
+ ```
142
+
143
+ ### Property Types
144
+
145
+ Use the `HemnetItemType` enum or strings:
146
+
147
+ ```python
148
+ # Using enum (recommended)
149
+ item_types = [HemnetItemType.VILLA, HemnetItemType.RADHUS]
150
+
151
+ # Using strings
152
+ item_types = ["villa", "radhus"]
153
+ ```
154
+
155
+ Available types:
156
+ - `VILLA` - Detached houses
157
+ - `RADHUS` - Townhouses
158
+ - `BOSTADSRATT` - Condominiums
159
+ - `FRITIDSHUS` - Vacation homes
160
+ - `TOMT` - Land plots
161
+ - `GARD` - Farms
162
+ - `OTHER` - Other property types
163
+
164
+ ## Data Structure
165
+
166
+ ### Sold Property Data
167
+
168
+ Each sold property dictionary contains:
169
+
170
+ ```python
171
+ {
172
+ 'id': str, # Hemnet ID
173
+ 'listing_id': str, # Listing identifier
174
+ 'address': str, # Street address
175
+ 'location': str, # Location description
176
+ 'housing_type': str, # Type of housing (Villa, Radhus, etc.)
177
+ 'rooms': int, # Number of rooms
178
+ 'living_area': str, # Living area with units
179
+ 'land_area': str, # Land area with units
180
+ 'asking_price': int, # Initial asking price in SEK
181
+ 'final_price': int, # Final sold price in SEK
182
+ 'price_change': str, # Price change information
183
+ 'sold_at': str, # Sale date (YYYY-MM-DD format)
184
+ 'broker': str, # Broker agency name
185
+ 'labels': list, # List of property labels/tags
186
+ }
187
+ ```
188
+
189
+ ### Current Listing Data
190
+
191
+ Each listing dictionary contains:
192
+
193
+ ```python
194
+ {
195
+ 'id': str, # Hemnet ID
196
+ 'address': str, # Street address
197
+ 'location': str, # Location description
198
+ 'housing_type': str, # Type of housing
199
+ 'rooms': int, # Number of rooms
200
+ 'living_area': str, # Living area with units
201
+ 'land_area': str, # Land area with units
202
+ 'asking_price': int, # Asking price in SEK
203
+ 'published_at': str, # Publication date (YYYY-MM-DD)
204
+ 'removed_before_showing': bool, # Removed before showing
205
+ 'new_construction': bool, # New construction flag
206
+ 'broker_name': str, # Broker name
207
+ 'broker_agent': str, # Broker agency name
208
+ 'labels': list, # List of property labels/tags
209
+ 'description': str, # Property description
210
+ }
211
+ ```
212
+
213
+ ## Finding Location IDs
214
+
215
+ To find Hemnet location IDs:
216
+
217
+ 1. Go to [Hemnet.se](https://www.hemnet.se)
218
+ 2. Search for your desired location
219
+ 3. Look at the URL - it contains `location_ids[]=XXXXX`
220
+ 4. Use that ID in your code
221
+
222
+ Example: For Stockholm `https://www.hemnet.se/bostader?location_ids[]=17744`, use `location_id="17744"`
223
+
224
+ ## Requirements
225
+
226
+ - Python 3.10+
227
+ - cloudscraper >= 1.2.71
228
+ - beautifulsoup4 >= 4.12.0
229
+ - requests >= 2.31.0
230
+
231
+ ## Contributing
232
+
233
+ Contributions are welcome! Please feel free to submit a Pull Request.
234
+
235
+ ## License
236
+
237
+ This project is licensed under the MIT License - see the LICENSE file for details.
238
+
239
+ ## Disclaimer
240
+
241
+ This package is created for exploring python and web technologies and learning purposes only. It is **not intended for production use** or commercial applications.
242
+
243
+ - This is an unofficial package and is not affiliated with or endorsed by Hemnet AB
244
+ - Always respect website terms of service and robots.txt directives
245
+ - Web scraping may be subject to legal restrictions in your jurisdiction
246
+ - Use at your own risk and responsibility
@@ -0,0 +1,8 @@
1
+ pyhemnet/__init__.py,sha256=-hgN9WjtWMIUYjao_srqn6xktng-51QjTbWhjQl1tdQ,284
2
+ pyhemnet/constants.py,sha256=3v7G619txdvZ-q1Zxd98kMc4bufnG4m9-5hfCuZJxeA,433
3
+ pyhemnet/hemnet.py,sha256=qmvvLVz4h9W2GqHj4EvpESDrBieCbPmzQP0nOqo1CKs,14127
4
+ pyhemnet-0.1.0.dist-info/licenses/LICENSE,sha256=jioEY5W5ia2JjY7G4XxApk1kQzME4_VzF7-loVgmxdk,1067
5
+ pyhemnet-0.1.0.dist-info/METADATA,sha256=0Ku68TDH1sDkmxZccgzJorXcqHRc28UQGFYeg5OIymo,7026
6
+ pyhemnet-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
+ pyhemnet-0.1.0.dist-info/top_level.txt,sha256=mYcB9sSjBU_pBJ-XRjSbxlW76JUS0g6F265MSHUD8jY,9
8
+ pyhemnet-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ningdp2012
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ pyhemnet