blocket-api 0.3.92__tar.gz → 0.3.93__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.
- {blocket_api-0.3.92 → blocket_api-0.3.93}/PKG-INFO +22 -10
- {blocket_api-0.3.92 → blocket_api-0.3.93}/README.md +20 -8
- {blocket_api-0.3.92 → blocket_api-0.3.93}/blocket_api/__init__.py +2 -0
- blocket_api-0.3.93/blocket_api/ad_parser.py +184 -0
- blocket_api-0.3.93/blocket_api/blocket.py +141 -0
- {blocket_api-0.3.92 → blocket_api-0.3.93}/blocket_api/constants.py +34 -1
- {blocket_api-0.3.92 → blocket_api-0.3.93}/blocket_api.egg-info/PKG-INFO +22 -10
- {blocket_api-0.3.92 → blocket_api-0.3.93}/blocket_api.egg-info/requires.txt +1 -1
- {blocket_api-0.3.92 → blocket_api-0.3.93}/pyproject.toml +2 -2
- blocket_api-0.3.92/blocket_api/ad_parser.py +0 -182
- blocket_api-0.3.92/blocket_api/blocket.py +0 -105
- {blocket_api-0.3.92 → blocket_api-0.3.93}/LICENSE +0 -0
- {blocket_api-0.3.92 → blocket_api-0.3.93}/blocket_api.egg-info/SOURCES.txt +0 -0
- {blocket_api-0.3.92 → blocket_api-0.3.93}/blocket_api.egg-info/dependency_links.txt +0 -0
- {blocket_api-0.3.92 → blocket_api-0.3.93}/blocket_api.egg-info/top_level.txt +0 -0
- {blocket_api-0.3.92 → blocket_api-0.3.93}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: blocket_api
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.93
|
|
4
4
|
Summary: A python API wrapper for blocket.se
|
|
5
5
|
Author-email: dunderrrrrr <emil.bjorkroth@gmail.com>
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -17,7 +17,7 @@ Requires-Dist: mkdocs-material>=9.6.21
|
|
|
17
17
|
Requires-Dist: mkdocs-swagger-ui-tag>=0.7.2
|
|
18
18
|
Requires-Dist: websockets>=15.0.1
|
|
19
19
|
Requires-Dist: beautifulsoup4>=4.14.2
|
|
20
|
-
Requires-Dist: ruff>=0.14.
|
|
20
|
+
Requires-Dist: ruff>=0.14.6
|
|
21
21
|
Dynamic: license-file
|
|
22
22
|
|
|
23
23
|
<p align="center">
|
|
@@ -32,9 +32,6 @@ BlocketAPI allows users to search [blocket.se](https://blocket.se/) for ads.
|
|
|
32
32
|
|
|
33
33
|
> Blocket is one of Sweden's largest online marketplaces. It was founded in 1996 and allows users to buy and sell a wide range of items, including cars, real estate, jobs, services, and second-hand goods. The platform is known for its extensive reach and user-friendly interface, making it a popular choice for Swedes looking to purchase or sell items quickly and efficiently.
|
|
34
34
|
|
|
35
|
-
> [!NOTE]
|
|
36
|
-
> Blocket recently released a new version of their site which caused this package to break. But do not fear, we are actively working to try and restore all functionality. More info in [issue #43](https://github.com/dunderrrrrr/blocket_api/issues/41).
|
|
37
|
-
|
|
38
35
|
## 🧑💻️ Install
|
|
39
36
|
|
|
40
37
|
Install BlocketAPI via PyPI...
|
|
@@ -62,13 +59,13 @@ api = BlocketAPI()
|
|
|
62
59
|
|
|
63
60
|
# search all of blocket
|
|
64
61
|
api.search(
|
|
65
|
-
"
|
|
62
|
+
"Tamagotchi",
|
|
66
63
|
sort_order=SortOrder.PRICE_ASC,
|
|
67
64
|
locations=[Location.STOCKHOLM, Location.UPPSALA],
|
|
68
|
-
category=Category.
|
|
65
|
+
category=Category.FRITID_HOBBY_OCH_UNDERHALLNING,
|
|
69
66
|
)
|
|
70
67
|
|
|
71
|
-
# search for cars
|
|
68
|
+
# search for cars
|
|
72
69
|
api.search_car(
|
|
73
70
|
"Audi", # query is optional
|
|
74
71
|
sort_order=CarSortOrder.MILEAGE_ASC,
|
|
@@ -80,11 +77,26 @@ api.search_car(
|
|
|
80
77
|
locations=[Location.STOCKHOLM],
|
|
81
78
|
)
|
|
82
79
|
|
|
83
|
-
#
|
|
84
|
-
from blocket_api import
|
|
80
|
+
# search for boats
|
|
81
|
+
from blocket_api import BoatType
|
|
82
|
+
|
|
83
|
+
api.search_boat(
|
|
84
|
+
"Mercury", # query is optional
|
|
85
|
+
types=[BoatType.DAYCRUISER],
|
|
86
|
+
locations=[Location.STOCKHOLM],
|
|
87
|
+
length_from=10,
|
|
88
|
+
length_to=15,
|
|
89
|
+
price_from=20000,
|
|
90
|
+
price_to=90000,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# get ad details
|
|
95
|
+
from blocket_api import CarAd, RecommerceAd, BoatAd
|
|
85
96
|
|
|
86
97
|
api.get_ad(RecommerceAd(12345678))
|
|
87
98
|
api.get_ad(CarAd(12345678))
|
|
99
|
+
api.get_ad(BoatAd(12345678))
|
|
88
100
|
```
|
|
89
101
|
|
|
90
102
|
## 📝 Notes
|
|
@@ -10,9 +10,6 @@ BlocketAPI allows users to search [blocket.se](https://blocket.se/) for ads.
|
|
|
10
10
|
|
|
11
11
|
> Blocket is one of Sweden's largest online marketplaces. It was founded in 1996 and allows users to buy and sell a wide range of items, including cars, real estate, jobs, services, and second-hand goods. The platform is known for its extensive reach and user-friendly interface, making it a popular choice for Swedes looking to purchase or sell items quickly and efficiently.
|
|
12
12
|
|
|
13
|
-
> [!NOTE]
|
|
14
|
-
> Blocket recently released a new version of their site which caused this package to break. But do not fear, we are actively working to try and restore all functionality. More info in [issue #43](https://github.com/dunderrrrrr/blocket_api/issues/41).
|
|
15
|
-
|
|
16
13
|
## 🧑💻️ Install
|
|
17
14
|
|
|
18
15
|
Install BlocketAPI via PyPI...
|
|
@@ -40,13 +37,13 @@ api = BlocketAPI()
|
|
|
40
37
|
|
|
41
38
|
# search all of blocket
|
|
42
39
|
api.search(
|
|
43
|
-
"
|
|
40
|
+
"Tamagotchi",
|
|
44
41
|
sort_order=SortOrder.PRICE_ASC,
|
|
45
42
|
locations=[Location.STOCKHOLM, Location.UPPSALA],
|
|
46
|
-
category=Category.
|
|
43
|
+
category=Category.FRITID_HOBBY_OCH_UNDERHALLNING,
|
|
47
44
|
)
|
|
48
45
|
|
|
49
|
-
# search for cars
|
|
46
|
+
# search for cars
|
|
50
47
|
api.search_car(
|
|
51
48
|
"Audi", # query is optional
|
|
52
49
|
sort_order=CarSortOrder.MILEAGE_ASC,
|
|
@@ -58,11 +55,26 @@ api.search_car(
|
|
|
58
55
|
locations=[Location.STOCKHOLM],
|
|
59
56
|
)
|
|
60
57
|
|
|
61
|
-
#
|
|
62
|
-
from blocket_api import
|
|
58
|
+
# search for boats
|
|
59
|
+
from blocket_api import BoatType
|
|
60
|
+
|
|
61
|
+
api.search_boat(
|
|
62
|
+
"Mercury", # query is optional
|
|
63
|
+
types=[BoatType.DAYCRUISER],
|
|
64
|
+
locations=[Location.STOCKHOLM],
|
|
65
|
+
length_from=10,
|
|
66
|
+
length_to=15,
|
|
67
|
+
price_from=20000,
|
|
68
|
+
price_to=90000,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# get ad details
|
|
73
|
+
from blocket_api import CarAd, RecommerceAd, BoatAd
|
|
63
74
|
|
|
64
75
|
api.get_ad(RecommerceAd(12345678))
|
|
65
76
|
api.get_ad(CarAd(12345678))
|
|
77
|
+
api.get_ad(BoatAd(12345678))
|
|
66
78
|
```
|
|
67
79
|
|
|
68
80
|
## 📝 Notes
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
from .ad_parser import BoatAd as BoatAd
|
|
1
2
|
from .ad_parser import CarAd as CarAd
|
|
2
3
|
from .ad_parser import RecommerceAd as RecommerceAd
|
|
3
4
|
from .blocket import BlocketAPI as BlocketAPI
|
|
4
5
|
from .blocket import Location as Location
|
|
6
|
+
from .constants import BoatType as BoatType
|
|
5
7
|
from .constants import CarColor as CarColor
|
|
6
8
|
from .constants import CarModel as CarModel
|
|
7
9
|
from .constants import CarSortOrder as CarSortOrder
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from bs4 import BeautifulSoup, NavigableString, Tag
|
|
7
|
+
from httpx import Response
|
|
8
|
+
|
|
9
|
+
from .constants import SITE_URL
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class RecommerceAd:
|
|
14
|
+
id: int
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def url(self) -> str:
|
|
18
|
+
return f"{SITE_URL}/recommerce/forsale/item/{self.id}"
|
|
19
|
+
|
|
20
|
+
def parse(self, response: Response) -> Any:
|
|
21
|
+
soup = BeautifulSoup(response.content, "html.parser")
|
|
22
|
+
json_script_tag = soup.select_one(
|
|
23
|
+
'script:-soup-contains("window.__staticRouterHydrationData")'
|
|
24
|
+
)
|
|
25
|
+
if not json_script_tag:
|
|
26
|
+
return {}
|
|
27
|
+
|
|
28
|
+
if match := re.search(r'JSON\.parse\("(.+)"\)', json_script_tag.text):
|
|
29
|
+
raw_json = match.group(1)
|
|
30
|
+
escaped = raw_json.encode("utf-8").decode("unicode_escape")
|
|
31
|
+
return json.loads(escaped.encode("latin1").decode("utf-8"))
|
|
32
|
+
else:
|
|
33
|
+
return {}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class MobilityAd:
|
|
38
|
+
id: int
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def url(self) -> str:
|
|
42
|
+
return f"{SITE_URL}/mobility/item/{self.id}"
|
|
43
|
+
|
|
44
|
+
def parse(self, response: Response) -> dict:
|
|
45
|
+
soup = BeautifulSoup(response.content, "html.parser")
|
|
46
|
+
grid = soup.find("div", class_="grid grid-cols-1 md:grid-cols-3 md:gap-x-32")
|
|
47
|
+
|
|
48
|
+
if not grid:
|
|
49
|
+
return {}
|
|
50
|
+
|
|
51
|
+
data: dict[str, Any] = {"url": self.url}
|
|
52
|
+
|
|
53
|
+
self._extract_title_and_subtitle(grid, data)
|
|
54
|
+
self._extract_quick_specs(grid, data)
|
|
55
|
+
self._extract_price(grid, data)
|
|
56
|
+
self._extract_description(grid, data)
|
|
57
|
+
self._extract_specifications(grid, data)
|
|
58
|
+
self._extract_seller_type(soup, data)
|
|
59
|
+
self._extract_ad_id(soup, data)
|
|
60
|
+
|
|
61
|
+
self.extend(data, soup, grid)
|
|
62
|
+
|
|
63
|
+
return data
|
|
64
|
+
|
|
65
|
+
def _extract_title_and_subtitle(self, grid: Tag, data: dict) -> None:
|
|
66
|
+
if title := grid.find("h1", class_=lambda x: x and "t1" in x):
|
|
67
|
+
data["title"] = title.get_text(strip=True)
|
|
68
|
+
|
|
69
|
+
if subtitle := grid.find("p", class_=lambda x: x and "s-text-subtle" in x):
|
|
70
|
+
data["subtitle"] = subtitle.get_text(strip=True)
|
|
71
|
+
|
|
72
|
+
def _extract_quick_specs(self, grid: Tag, data: dict) -> None:
|
|
73
|
+
if specs_grid := grid.find(
|
|
74
|
+
"div", class_=lambda x: x and "grid" in x and "gap-24" in x
|
|
75
|
+
):
|
|
76
|
+
for item in specs_grid.find_all("div", class_="flex gap-16 hyphens-auto"):
|
|
77
|
+
label = item.find("span", class_="s-text-subtle")
|
|
78
|
+
value = item.find("p", class_="m-0 font-bold")
|
|
79
|
+
if label and value:
|
|
80
|
+
label_text = label.get_text(strip=True)
|
|
81
|
+
value_text = value.get_text(strip=True)
|
|
82
|
+
key = getattr(self, "quick_spec_mapping", {}).get(
|
|
83
|
+
label_text, label_text.lower().replace(" ", "_")
|
|
84
|
+
)
|
|
85
|
+
data[key] = value_text
|
|
86
|
+
|
|
87
|
+
def _extract_price(self, grid: Tag, data: dict) -> None:
|
|
88
|
+
if price_section := grid.find("div", class_="border-t pt-40 mt-40"):
|
|
89
|
+
price_elem = price_section.find("span", class_="t2")
|
|
90
|
+
if price_elem:
|
|
91
|
+
data["price"] = price_elem.get_text(strip=True)
|
|
92
|
+
|
|
93
|
+
def _extract_description(self, grid: Tag, data: dict) -> None:
|
|
94
|
+
for section in grid.find_all("section", class_="border-t mt-40 pt-40"):
|
|
95
|
+
h2 = section.find("h2", class_="t3 mb-0")
|
|
96
|
+
if h2 and "beskrivning" in h2.get_text(strip=True).lower():
|
|
97
|
+
desc_div = section.find("div", class_="whitespace-pre-wrap")
|
|
98
|
+
if desc_div:
|
|
99
|
+
data["description"] = desc_div.get_text(strip=True)
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
def _extract_specifications(self, grid: Tag, data: dict) -> None:
|
|
103
|
+
specs_section = grid.find("section", class_="key-info-section")
|
|
104
|
+
if not specs_section:
|
|
105
|
+
return
|
|
106
|
+
dl = specs_section.find("dl")
|
|
107
|
+
if not dl:
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
specifications = {}
|
|
111
|
+
for div in dl.find_all("div", style="break-inside:avoid-column"):
|
|
112
|
+
dt = div.find("dt")
|
|
113
|
+
dd = div.find("dd")
|
|
114
|
+
if dt and dd:
|
|
115
|
+
specifications[dt.get_text(strip=True)] = dd.get_text(strip=True)
|
|
116
|
+
|
|
117
|
+
if specifications:
|
|
118
|
+
data["specifications"] = specifications
|
|
119
|
+
|
|
120
|
+
def _extract_seller_type(self, soup: BeautifulSoup, data: dict) -> None:
|
|
121
|
+
# Look for a div whose class contains "dealer"
|
|
122
|
+
data["seller_type"] = (
|
|
123
|
+
"dealer"
|
|
124
|
+
if soup.find("div", class_=lambda x: x and "dealer" in x.lower())
|
|
125
|
+
else "private"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def _extract_ad_id(self, soup: BeautifulSoup, data: dict) -> None:
|
|
129
|
+
label = next(
|
|
130
|
+
(
|
|
131
|
+
p
|
|
132
|
+
for p in soup.find_all("p")
|
|
133
|
+
if isinstance(p.string, NavigableString) and "Annons-ID" in p.string
|
|
134
|
+
),
|
|
135
|
+
None,
|
|
136
|
+
)
|
|
137
|
+
if label:
|
|
138
|
+
elem = label.find_next("p")
|
|
139
|
+
if elem:
|
|
140
|
+
data["ad_id"] = elem.get_text(strip=True)
|
|
141
|
+
|
|
142
|
+
def extend(self, data: dict, soup: BeautifulSoup, grid: Tag) -> None:
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class CarAd(MobilityAd):
|
|
147
|
+
quick_spec_mapping = {
|
|
148
|
+
"Modellår": "model_year",
|
|
149
|
+
"Miltal": "mileage",
|
|
150
|
+
"Växellåda": "transmission",
|
|
151
|
+
"Drivmedel": "fuel",
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
def extend(self, data: dict, soup: BeautifulSoup, grid: Tag) -> None:
|
|
155
|
+
equip_section = next(
|
|
156
|
+
(
|
|
157
|
+
h2
|
|
158
|
+
for h2 in grid.find_all("h2")
|
|
159
|
+
if isinstance(h2.string, NavigableString) and "Utrustning" in h2.string
|
|
160
|
+
),
|
|
161
|
+
None,
|
|
162
|
+
)
|
|
163
|
+
if equip_section:
|
|
164
|
+
parent = equip_section.find_parent("section")
|
|
165
|
+
if parent:
|
|
166
|
+
items = [li.get_text(strip=True) for li in parent.find_all("li")]
|
|
167
|
+
if items:
|
|
168
|
+
data["equipment"] = items
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class BoatAd(MobilityAd):
|
|
172
|
+
quick_spec_mapping = {
|
|
173
|
+
"Modellår": "model_year",
|
|
174
|
+
"Längd": "length",
|
|
175
|
+
"Motortyp": "engine_type",
|
|
176
|
+
"Säten": "seats",
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
def extend(self, data: dict, soup: BeautifulSoup, grid: Tag) -> None:
|
|
180
|
+
if place_h2 := soup.find(
|
|
181
|
+
"h2", text=lambda t: isinstance(t, NavigableString) and "Plats" in t
|
|
182
|
+
):
|
|
183
|
+
if link := place_h2.find_parent():
|
|
184
|
+
data["location"] = link.get_text(strip=True)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
from httpx import Response
|
|
8
|
+
|
|
9
|
+
from .ad_parser import BoatAd, CarAd, RecommerceAd
|
|
10
|
+
from .constants import (
|
|
11
|
+
HEADERS,
|
|
12
|
+
SITE_URL,
|
|
13
|
+
BoatType,
|
|
14
|
+
CarColor,
|
|
15
|
+
CarModel,
|
|
16
|
+
CarSortOrder,
|
|
17
|
+
CarTransmission,
|
|
18
|
+
Category,
|
|
19
|
+
Location,
|
|
20
|
+
SortOrder,
|
|
21
|
+
SubCategory,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class QueryParam:
|
|
27
|
+
name: str
|
|
28
|
+
value: str | int
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _request(*, url: str, params: list[QueryParam]) -> Response:
|
|
32
|
+
response = httpx.get(
|
|
33
|
+
url,
|
|
34
|
+
headers=HEADERS,
|
|
35
|
+
params=[(param.name, param.value) for param in params],
|
|
36
|
+
)
|
|
37
|
+
response.raise_for_status()
|
|
38
|
+
return response
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class BlocketAPI:
|
|
43
|
+
def search(
|
|
44
|
+
self,
|
|
45
|
+
query: str,
|
|
46
|
+
*,
|
|
47
|
+
sort_order: SortOrder = SortOrder.RELEVANCE,
|
|
48
|
+
locations: list[Location] = [],
|
|
49
|
+
category: Category | None = None,
|
|
50
|
+
sub_category: SubCategory | None = None,
|
|
51
|
+
) -> Any:
|
|
52
|
+
if category and sub_category:
|
|
53
|
+
raise AssertionError("Cannot specify both category and sub_categories")
|
|
54
|
+
|
|
55
|
+
url = f"{SITE_URL}/recommerce-search-page/api/search/SEARCH_ID_BAP_COMMON"
|
|
56
|
+
|
|
57
|
+
param_dict = {
|
|
58
|
+
"q": query,
|
|
59
|
+
"sort": sort_order.value,
|
|
60
|
+
"category": category.value if category else None,
|
|
61
|
+
"sub_category": sub_category.value if sub_category else None,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
params = [QueryParam(k, v) for k, v in param_dict.items() if v is not None]
|
|
65
|
+
|
|
66
|
+
params.extend(QueryParam("location", loc.value) for loc in locations)
|
|
67
|
+
|
|
68
|
+
return _request(url=url, params=params).json()
|
|
69
|
+
|
|
70
|
+
def search_car(
|
|
71
|
+
self,
|
|
72
|
+
query: str | None = None,
|
|
73
|
+
*,
|
|
74
|
+
sort_order: CarSortOrder = CarSortOrder.RELEVANCE,
|
|
75
|
+
locations: list[Location] = [],
|
|
76
|
+
models: list[CarModel] = [],
|
|
77
|
+
price_from: int | None = None,
|
|
78
|
+
price_to: int | None = None,
|
|
79
|
+
year_from: int | None = None,
|
|
80
|
+
year_to: int | None = None,
|
|
81
|
+
milage_from: int | None = None,
|
|
82
|
+
milage_to: int | None = None,
|
|
83
|
+
colors: list[CarColor] = [],
|
|
84
|
+
transmissions: list[CarTransmission] = [],
|
|
85
|
+
) -> Any:
|
|
86
|
+
url = f"{SITE_URL}/mobility/search/api/search/SEARCH_ID_CAR_USED"
|
|
87
|
+
|
|
88
|
+
param_dict = {
|
|
89
|
+
"q": query,
|
|
90
|
+
"sort": sort_order.value,
|
|
91
|
+
"price_from": price_from,
|
|
92
|
+
"price_to": price_to,
|
|
93
|
+
"year_from": year_from,
|
|
94
|
+
"year_to": year_to,
|
|
95
|
+
"milage_from": milage_from,
|
|
96
|
+
"milage_to": milage_to,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
params = [QueryParam(k, v) for k, v in param_dict.items() if v is not None]
|
|
100
|
+
|
|
101
|
+
# Multi-value params
|
|
102
|
+
params.extend(QueryParam("location", loc.value) for loc in locations)
|
|
103
|
+
params.extend(QueryParam("make", model.value) for model in models)
|
|
104
|
+
params.extend(QueryParam("exterior_colour", color.value) for color in colors)
|
|
105
|
+
params.extend(QueryParam("transmission", t.value) for t in transmissions)
|
|
106
|
+
|
|
107
|
+
return _request(url=url, params=params).json()
|
|
108
|
+
|
|
109
|
+
def search_boat(
|
|
110
|
+
self,
|
|
111
|
+
query: str | None = None,
|
|
112
|
+
*,
|
|
113
|
+
sort_order: CarSortOrder = CarSortOrder.RELEVANCE,
|
|
114
|
+
types: list[BoatType] = [],
|
|
115
|
+
locations: list[Location] = [],
|
|
116
|
+
price_from: int | None = None,
|
|
117
|
+
price_to: int | None = None,
|
|
118
|
+
length_from: int | None = None,
|
|
119
|
+
length_to: int | None = None,
|
|
120
|
+
) -> Any:
|
|
121
|
+
url = f"{SITE_URL}/mobility/search/api/search/SEARCH_ID_BOAT_USED"
|
|
122
|
+
|
|
123
|
+
param_dict = {
|
|
124
|
+
"q": query,
|
|
125
|
+
"sort": sort_order.value,
|
|
126
|
+
"price_from": price_from,
|
|
127
|
+
"price_to": price_to,
|
|
128
|
+
"length_feet_from": length_from,
|
|
129
|
+
"length_feet_to": length_to,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
params = [QueryParam(k, v) for k, v in param_dict.items() if v is not None]
|
|
133
|
+
|
|
134
|
+
params.extend(QueryParam("class", t.value) for t in types)
|
|
135
|
+
params.extend(QueryParam("location", loc.value) for loc in locations)
|
|
136
|
+
|
|
137
|
+
return _request(url=url, params=params).json()
|
|
138
|
+
|
|
139
|
+
def get_ad(self, ad: RecommerceAd | CarAd | BoatAd) -> dict:
|
|
140
|
+
response = _request(url=ad.url, params=[])
|
|
141
|
+
return ad.parse(response)
|
|
@@ -21,7 +21,7 @@ class CarSortOrder(StrEnum):
|
|
|
21
21
|
PUBLISHED_DESC = "PUBLISHED_DESC"
|
|
22
22
|
PUBLISHED_ASC = "PUBLISHED_ASC"
|
|
23
23
|
|
|
24
|
-
#
|
|
24
|
+
# car specific
|
|
25
25
|
MILEAGE_DESC = "MILEAGE_DESC"
|
|
26
26
|
MILEAGE_ASC = "MILEAGE_ASC"
|
|
27
27
|
MODEL = "MODEL"
|
|
@@ -29,6 +29,22 @@ class CarSortOrder(StrEnum):
|
|
|
29
29
|
YEAR_ASC = "YEAR_ASC"
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
class BoatSortOrder(StrEnum):
|
|
33
|
+
RELEVANCE = "RELEVANCE"
|
|
34
|
+
PRICE_DESC = "PRICE_DESC"
|
|
35
|
+
PRICE_ASC = "PRICE_ASC"
|
|
36
|
+
PUBLISHED_DESC = "PUBLISHED_DESC"
|
|
37
|
+
PUBLISHED_ASC = "PUBLISHED_ASC"
|
|
38
|
+
|
|
39
|
+
# boat specific
|
|
40
|
+
LENGTH_DESC = "LENGTH_DESC"
|
|
41
|
+
LENGTH_ASC = "LENGTH_ASC"
|
|
42
|
+
SPEED_DESC = "SPEED_DESC"
|
|
43
|
+
SPEED_ASC = "SPEED_ASC"
|
|
44
|
+
YEAR_DESC = "YEAR_DESC"
|
|
45
|
+
YEAR_ASC = "YEAR_ASC"
|
|
46
|
+
|
|
47
|
+
|
|
32
48
|
class Location(StrEnum):
|
|
33
49
|
BLEKINGE = "0.300010"
|
|
34
50
|
DALARNA = "0.300020"
|
|
@@ -348,3 +364,20 @@ class CarColor(IntEnum):
|
|
|
348
364
|
class CarTransmission(IntEnum):
|
|
349
365
|
AUTOMATIC = 2
|
|
350
366
|
MANUAL = 1
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
class BoatType(IntEnum):
|
|
370
|
+
BOWRIDER = 7961
|
|
371
|
+
DAYCRUISER = 2184
|
|
372
|
+
FISKEBAT_ARBETSBAT = 6921
|
|
373
|
+
HYTTBAT = 7962
|
|
374
|
+
KABINBAT = 6923
|
|
375
|
+
POWERBOAT = 7960
|
|
376
|
+
RIB = 7343
|
|
377
|
+
SEGELBAT_MOTORSEGLARE = 2188
|
|
378
|
+
SMABAT_GUMMIBAT = 2186
|
|
379
|
+
SNIPA = 6922
|
|
380
|
+
STYRPULPETBAT = 3827
|
|
381
|
+
VATTENSKOTER = 2190
|
|
382
|
+
YACHT = 7959
|
|
383
|
+
ANNAT = 2258
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: blocket_api
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.93
|
|
4
4
|
Summary: A python API wrapper for blocket.se
|
|
5
5
|
Author-email: dunderrrrrr <emil.bjorkroth@gmail.com>
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -17,7 +17,7 @@ Requires-Dist: mkdocs-material>=9.6.21
|
|
|
17
17
|
Requires-Dist: mkdocs-swagger-ui-tag>=0.7.2
|
|
18
18
|
Requires-Dist: websockets>=15.0.1
|
|
19
19
|
Requires-Dist: beautifulsoup4>=4.14.2
|
|
20
|
-
Requires-Dist: ruff>=0.14.
|
|
20
|
+
Requires-Dist: ruff>=0.14.6
|
|
21
21
|
Dynamic: license-file
|
|
22
22
|
|
|
23
23
|
<p align="center">
|
|
@@ -32,9 +32,6 @@ BlocketAPI allows users to search [blocket.se](https://blocket.se/) for ads.
|
|
|
32
32
|
|
|
33
33
|
> Blocket is one of Sweden's largest online marketplaces. It was founded in 1996 and allows users to buy and sell a wide range of items, including cars, real estate, jobs, services, and second-hand goods. The platform is known for its extensive reach and user-friendly interface, making it a popular choice for Swedes looking to purchase or sell items quickly and efficiently.
|
|
34
34
|
|
|
35
|
-
> [!NOTE]
|
|
36
|
-
> Blocket recently released a new version of their site which caused this package to break. But do not fear, we are actively working to try and restore all functionality. More info in [issue #43](https://github.com/dunderrrrrr/blocket_api/issues/41).
|
|
37
|
-
|
|
38
35
|
## 🧑💻️ Install
|
|
39
36
|
|
|
40
37
|
Install BlocketAPI via PyPI...
|
|
@@ -62,13 +59,13 @@ api = BlocketAPI()
|
|
|
62
59
|
|
|
63
60
|
# search all of blocket
|
|
64
61
|
api.search(
|
|
65
|
-
"
|
|
62
|
+
"Tamagotchi",
|
|
66
63
|
sort_order=SortOrder.PRICE_ASC,
|
|
67
64
|
locations=[Location.STOCKHOLM, Location.UPPSALA],
|
|
68
|
-
category=Category.
|
|
65
|
+
category=Category.FRITID_HOBBY_OCH_UNDERHALLNING,
|
|
69
66
|
)
|
|
70
67
|
|
|
71
|
-
# search for cars
|
|
68
|
+
# search for cars
|
|
72
69
|
api.search_car(
|
|
73
70
|
"Audi", # query is optional
|
|
74
71
|
sort_order=CarSortOrder.MILEAGE_ASC,
|
|
@@ -80,11 +77,26 @@ api.search_car(
|
|
|
80
77
|
locations=[Location.STOCKHOLM],
|
|
81
78
|
)
|
|
82
79
|
|
|
83
|
-
#
|
|
84
|
-
from blocket_api import
|
|
80
|
+
# search for boats
|
|
81
|
+
from blocket_api import BoatType
|
|
82
|
+
|
|
83
|
+
api.search_boat(
|
|
84
|
+
"Mercury", # query is optional
|
|
85
|
+
types=[BoatType.DAYCRUISER],
|
|
86
|
+
locations=[Location.STOCKHOLM],
|
|
87
|
+
length_from=10,
|
|
88
|
+
length_to=15,
|
|
89
|
+
price_from=20000,
|
|
90
|
+
price_to=90000,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# get ad details
|
|
95
|
+
from blocket_api import CarAd, RecommerceAd, BoatAd
|
|
85
96
|
|
|
86
97
|
api.get_ad(RecommerceAd(12345678))
|
|
87
98
|
api.get_ad(CarAd(12345678))
|
|
99
|
+
api.get_ad(BoatAd(12345678))
|
|
88
100
|
```
|
|
89
101
|
|
|
90
102
|
## 📝 Notes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "blocket_api"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.93"
|
|
4
4
|
description = "A python API wrapper for blocket.se"
|
|
5
5
|
authors = [{name = "dunderrrrrr", email = "emil.bjorkroth@gmail.com"}]
|
|
6
6
|
readme = "README.md"
|
|
@@ -17,7 +17,7 @@ dependencies = [
|
|
|
17
17
|
"mkdocs-swagger-ui-tag>=0.7.2",
|
|
18
18
|
"websockets>=15.0.1",
|
|
19
19
|
"beautifulsoup4>=4.14.2",
|
|
20
|
-
"ruff>=0.14.
|
|
20
|
+
"ruff>=0.14.6",
|
|
21
21
|
]
|
|
22
22
|
|
|
23
23
|
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import re
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from typing import Any
|
|
5
|
-
|
|
6
|
-
from bs4 import BeautifulSoup, Tag
|
|
7
|
-
from httpx import Response
|
|
8
|
-
|
|
9
|
-
from .constants import SITE_URL
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@dataclass(frozen=True)
|
|
13
|
-
class RecommerceAd:
|
|
14
|
-
id: int
|
|
15
|
-
|
|
16
|
-
@property
|
|
17
|
-
def url(self) -> str:
|
|
18
|
-
return f"{SITE_URL}/recommerce/forsale/item/{self.id}"
|
|
19
|
-
|
|
20
|
-
def parse(self, response: Response) -> Any:
|
|
21
|
-
soup = BeautifulSoup(response.content, "html.parser")
|
|
22
|
-
json_script_tag = soup.select_one(
|
|
23
|
-
'script:-soup-contains("window.__staticRouterHydrationData")'
|
|
24
|
-
)
|
|
25
|
-
if not json_script_tag:
|
|
26
|
-
return {}
|
|
27
|
-
|
|
28
|
-
if match := re.search(r'JSON\.parse\("(.+)"\)', json_script_tag.text):
|
|
29
|
-
raw_json = match.group(1)
|
|
30
|
-
escaped = raw_json.encode("utf-8").decode("unicode_escape")
|
|
31
|
-
return json.loads(escaped.encode("latin1").decode("utf-8"))
|
|
32
|
-
else:
|
|
33
|
-
return {}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
@dataclass(frozen=True)
|
|
37
|
-
class CarAd:
|
|
38
|
-
id: int
|
|
39
|
-
|
|
40
|
-
@property
|
|
41
|
-
def url(self) -> str:
|
|
42
|
-
return f"{SITE_URL}/mobility/item/{self.id}"
|
|
43
|
-
|
|
44
|
-
def parse(self, response: Response) -> dict:
|
|
45
|
-
soup = BeautifulSoup(response.content, "html.parser")
|
|
46
|
-
grid = soup.find("div", class_="grid grid-cols-1 md:grid-cols-3 md:gap-x-32")
|
|
47
|
-
|
|
48
|
-
if not grid:
|
|
49
|
-
return {}
|
|
50
|
-
|
|
51
|
-
data: dict[str, Any] = {"url": self.url}
|
|
52
|
-
|
|
53
|
-
self._extract_title_and_subtitle(grid, data)
|
|
54
|
-
self._extract_quick_specs(grid, data)
|
|
55
|
-
self._extract_price(grid, data)
|
|
56
|
-
self._extract_description(grid, data)
|
|
57
|
-
self._extract_specifications(grid, data)
|
|
58
|
-
self._extract_equipment(grid, data)
|
|
59
|
-
self._extract_seller_type(soup, data)
|
|
60
|
-
self._extract_ad_id(soup, data)
|
|
61
|
-
|
|
62
|
-
return data
|
|
63
|
-
|
|
64
|
-
def _extract_title_and_subtitle(self, grid: Tag, data: dict[str, Any]) -> None:
|
|
65
|
-
if title := grid.find("h1", class_=lambda x: x and "t1" in x):
|
|
66
|
-
data["title"] = title.get_text(strip=True)
|
|
67
|
-
|
|
68
|
-
if subtitle := grid.find(
|
|
69
|
-
"p", class_=lambda x: x and "s-text-subtle" in x and "mt-8" in x
|
|
70
|
-
):
|
|
71
|
-
data["subtitle"] = subtitle.get_text(strip=True)
|
|
72
|
-
|
|
73
|
-
def _extract_quick_specs(self, grid: Tag, data: dict[str, Any]) -> None:
|
|
74
|
-
if specs_grid := grid.find(
|
|
75
|
-
"div", class_=lambda x: x and "grid" in x and "gap-24" in x
|
|
76
|
-
):
|
|
77
|
-
spec_items = specs_grid.find_all("div", class_="flex gap-16 hyphens-auto")
|
|
78
|
-
for item in spec_items:
|
|
79
|
-
label = item.find("span", class_="s-text-subtle")
|
|
80
|
-
value = item.find("p", class_="m-0 font-bold")
|
|
81
|
-
if label and value:
|
|
82
|
-
label_text = label.get_text(strip=True)
|
|
83
|
-
value_text = value.get_text(strip=True)
|
|
84
|
-
|
|
85
|
-
key_mapping = {
|
|
86
|
-
"Modellår": "model_year",
|
|
87
|
-
"Miltal": "mileage",
|
|
88
|
-
"Växellåda": "transmission",
|
|
89
|
-
"Drivmedel": "fuel",
|
|
90
|
-
}
|
|
91
|
-
key = key_mapping.get(
|
|
92
|
-
label_text, label_text.lower().replace(" ", "_")
|
|
93
|
-
)
|
|
94
|
-
data[key] = value_text
|
|
95
|
-
|
|
96
|
-
def _extract_price(self, grid: Tag, data: dict[str, Any]) -> None:
|
|
97
|
-
if price_section := grid.find("div", class_="border-t pt-40 mt-40"):
|
|
98
|
-
price_labels = price_section.find_all("p", class_="s-text-subtle mb-0")
|
|
99
|
-
for price_label in price_labels:
|
|
100
|
-
label_text = price_label.get_text(strip=True).lower()
|
|
101
|
-
if "pris" in label_text:
|
|
102
|
-
price_elem = price_section.find("span", class_="t2")
|
|
103
|
-
if price_elem:
|
|
104
|
-
data["price"] = price_elem.get_text(strip=True)
|
|
105
|
-
break
|
|
106
|
-
elif "månadskostnad" in label_text:
|
|
107
|
-
monthly_elem = price_section.find("h2", class_="t2")
|
|
108
|
-
if monthly_elem:
|
|
109
|
-
data["monthly_cost"] = monthly_elem.get_text(strip=True)
|
|
110
|
-
break
|
|
111
|
-
|
|
112
|
-
def _extract_description(self, grid: Tag, data: dict[str, Any]) -> None:
|
|
113
|
-
desc_sections = grid.find_all("section", class_="border-t mt-40 pt-40")
|
|
114
|
-
for section in desc_sections:
|
|
115
|
-
h2 = section.find("h2", class_="t3 mb-0")
|
|
116
|
-
if h2 and "beskrivning" in h2.get_text(strip=True).lower():
|
|
117
|
-
desc_div = section.find("div", class_="whitespace-pre-wrap")
|
|
118
|
-
if desc_div:
|
|
119
|
-
data["description"] = desc_div.get_text(strip=True)
|
|
120
|
-
break
|
|
121
|
-
|
|
122
|
-
def _extract_specifications(self, grid: Tag, data: dict[str, Any]) -> None:
|
|
123
|
-
specs_section = grid.find("section", class_="key-info-section")
|
|
124
|
-
if not specs_section:
|
|
125
|
-
return
|
|
126
|
-
|
|
127
|
-
dl = specs_section.find("dl")
|
|
128
|
-
if not dl:
|
|
129
|
-
return
|
|
130
|
-
|
|
131
|
-
specifications: dict[str, str] = {}
|
|
132
|
-
divs = dl.find_all("div", style="break-inside:avoid-column")
|
|
133
|
-
for div in divs:
|
|
134
|
-
dt = div.find("dt")
|
|
135
|
-
dd = div.find("dd")
|
|
136
|
-
if dt and dd:
|
|
137
|
-
key_text = dt.get_text(strip=True)
|
|
138
|
-
value_text = dd.get_text(strip=True)
|
|
139
|
-
specifications[key_text] = value_text
|
|
140
|
-
|
|
141
|
-
if specifications:
|
|
142
|
-
data["specifications"] = specifications
|
|
143
|
-
|
|
144
|
-
def _extract_equipment(self, grid: Tag, data: dict[str, Any]) -> None:
|
|
145
|
-
equipment_section: Tag | None = None
|
|
146
|
-
for section in grid.find_all("section", class_="border-t pt-40 mt-40"):
|
|
147
|
-
h2 = section.find("h2", class_="t3 mb-0")
|
|
148
|
-
if h2 and "utrustning" in h2.get_text(strip=True).lower():
|
|
149
|
-
equipment_section = section
|
|
150
|
-
break
|
|
151
|
-
|
|
152
|
-
if not equipment_section:
|
|
153
|
-
return
|
|
154
|
-
|
|
155
|
-
equipment_list = equipment_section.find("ul")
|
|
156
|
-
if equipment_list:
|
|
157
|
-
equipment_items: list[str] = [
|
|
158
|
-
li.get_text(strip=True) for li in equipment_list.find_all("li")
|
|
159
|
-
]
|
|
160
|
-
if equipment_items:
|
|
161
|
-
data["equipment"] = equipment_items
|
|
162
|
-
|
|
163
|
-
def _extract_seller_type(self, soup: BeautifulSoup, data: dict[str, Any]) -> None:
|
|
164
|
-
seller_type = (
|
|
165
|
-
"dealer"
|
|
166
|
-
if soup.find("div", class_=lambda x: x and "dealer" in str(x).lower())
|
|
167
|
-
else "private"
|
|
168
|
-
)
|
|
169
|
-
data["seller_type"] = seller_type
|
|
170
|
-
|
|
171
|
-
def _extract_ad_id(self, soup: BeautifulSoup, data: dict[str, Any]) -> None:
|
|
172
|
-
ad_info_divs = soup.find_all(
|
|
173
|
-
"div", class_="text-m flex md:flex-row flex-col md:gap-x-56 gap-y-16"
|
|
174
|
-
)
|
|
175
|
-
for div in ad_info_divs:
|
|
176
|
-
ad_id_labels = div.find_all("p", class_="s-text-subtle mb-0")
|
|
177
|
-
for ad_id_label in ad_id_labels:
|
|
178
|
-
if "Annons-ID" in ad_id_label.get_text(strip=True):
|
|
179
|
-
ad_id_elem = ad_id_label.find_next_sibling("p")
|
|
180
|
-
if ad_id_elem:
|
|
181
|
-
data["ad_id"] = ad_id_elem.get_text(strip=True)
|
|
182
|
-
break
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from typing import Any
|
|
5
|
-
|
|
6
|
-
import httpx
|
|
7
|
-
from httpx import Response
|
|
8
|
-
|
|
9
|
-
from .ad_parser import CarAd, RecommerceAd
|
|
10
|
-
from .constants import (
|
|
11
|
-
HEADERS,
|
|
12
|
-
SITE_URL,
|
|
13
|
-
CarColor,
|
|
14
|
-
CarModel,
|
|
15
|
-
CarSortOrder,
|
|
16
|
-
CarTransmission,
|
|
17
|
-
Category,
|
|
18
|
-
Location,
|
|
19
|
-
SortOrder,
|
|
20
|
-
SubCategory,
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@dataclass(frozen=True)
|
|
25
|
-
class QueryParam:
|
|
26
|
-
name: str
|
|
27
|
-
value: str | int
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def _request(
|
|
31
|
-
*,
|
|
32
|
-
url: str,
|
|
33
|
-
params: list[QueryParam],
|
|
34
|
-
) -> Response:
|
|
35
|
-
response = httpx.get(
|
|
36
|
-
url,
|
|
37
|
-
headers=HEADERS,
|
|
38
|
-
params=[(param.name, param.value) for param in params],
|
|
39
|
-
)
|
|
40
|
-
response.raise_for_status()
|
|
41
|
-
return response
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
@dataclass(frozen=True)
|
|
45
|
-
class BlocketAPI:
|
|
46
|
-
def search(
|
|
47
|
-
self,
|
|
48
|
-
query: str,
|
|
49
|
-
*,
|
|
50
|
-
sort_order: SortOrder = SortOrder.RELEVANCE,
|
|
51
|
-
locations: list[Location] = [],
|
|
52
|
-
category: Category | None = None,
|
|
53
|
-
sub_category: SubCategory | None = None,
|
|
54
|
-
) -> Any:
|
|
55
|
-
if category and sub_category:
|
|
56
|
-
raise AssertionError("Cannot specify both category and sub_categories")
|
|
57
|
-
|
|
58
|
-
url = f"{SITE_URL}/recommerce-search-page/api/search/SEARCH_ID_BAP_COMMON"
|
|
59
|
-
params = [
|
|
60
|
-
QueryParam("q", query),
|
|
61
|
-
QueryParam("sort", sort_order.value),
|
|
62
|
-
*[QueryParam("location", location.value) for location in locations],
|
|
63
|
-
*([QueryParam("category", category.value)] if category else []),
|
|
64
|
-
*([QueryParam("sub_category", sub_category.value)] if sub_category else []),
|
|
65
|
-
]
|
|
66
|
-
|
|
67
|
-
return _request(url=url, params=params).json()
|
|
68
|
-
|
|
69
|
-
def search_car(
|
|
70
|
-
self,
|
|
71
|
-
query: str | None = None,
|
|
72
|
-
*,
|
|
73
|
-
sort_order: CarSortOrder = CarSortOrder.RELEVANCE,
|
|
74
|
-
locations: list[Location] = [],
|
|
75
|
-
models: list[CarModel] = [],
|
|
76
|
-
price_from: int | None = None,
|
|
77
|
-
price_to: int | None = None,
|
|
78
|
-
year_from: int | None = None,
|
|
79
|
-
year_to: int | None = None,
|
|
80
|
-
milage_from: int | None = None,
|
|
81
|
-
milage_to: int | None = None,
|
|
82
|
-
colors: list[CarColor] = [],
|
|
83
|
-
transmissions: list[CarTransmission] = [],
|
|
84
|
-
) -> Any:
|
|
85
|
-
url = f"{SITE_URL}/mobility/search/api/search/SEARCH_ID_CAR_USED"
|
|
86
|
-
params = [
|
|
87
|
-
*([QueryParam("q", query)] if query else []),
|
|
88
|
-
QueryParam("sort", sort_order.value),
|
|
89
|
-
*[QueryParam("location", location.value) for location in locations],
|
|
90
|
-
*[QueryParam("make", model.value) for model in models],
|
|
91
|
-
*([QueryParam("price_from", price_from)] if price_from else []),
|
|
92
|
-
*([QueryParam("price_to", price_to)] if price_to else []),
|
|
93
|
-
*([QueryParam("year_from", year_from)] if year_from else []),
|
|
94
|
-
*([QueryParam("year_to", year_to)] if year_to else []),
|
|
95
|
-
*([QueryParam("milage_from", milage_from)] if milage_from else []),
|
|
96
|
-
*([QueryParam("milage_to", milage_to)] if milage_to else []),
|
|
97
|
-
*[QueryParam("exterior_colour", color.value) for color in colors],
|
|
98
|
-
*[QueryParam("transmission", t.value) for t in transmissions],
|
|
99
|
-
]
|
|
100
|
-
|
|
101
|
-
return _request(url=url, params=params).json()
|
|
102
|
-
|
|
103
|
-
def get_ad(self, ad: RecommerceAd | CarAd) -> dict:
|
|
104
|
-
response = _request(url=ad.url, params=[])
|
|
105
|
-
return ad.parse(response)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|