blocket-api 0.3.91__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: blocket_api
3
- Version: 0.3.91
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
@@ -16,7 +16,8 @@ Requires-Dist: mkdocs>=1.6.1
16
16
  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
- Requires-Dist: ruff>=0.14.5
19
+ Requires-Dist: beautifulsoup4>=4.14.2
20
+ Requires-Dist: ruff>=0.14.6
20
21
  Dynamic: license-file
21
22
 
22
23
  <p align="center">
@@ -31,9 +32,6 @@ BlocketAPI allows users to search [blocket.se](https://blocket.se/) for ads.
31
32
 
32
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.
33
34
 
34
- > [!NOTE]
35
- > 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).
36
-
37
35
  ## 🧑‍💻️ Install
38
36
 
39
37
  Install BlocketAPI via PyPI...
@@ -59,25 +57,46 @@ from blocket_api import (
59
57
 
60
58
  api = BlocketAPI()
61
59
 
62
- # will search all of blocket
63
- result = api.search(
64
- "Vinterdäck Audi",
60
+ # search all of blocket
61
+ api.search(
62
+ "Tamagotchi",
65
63
  sort_order=SortOrder.PRICE_ASC,
66
64
  locations=[Location.STOCKHOLM, Location.UPPSALA],
67
- category=Category.FORDONSTILLBEHOR,
65
+ category=Category.FRITID_HOBBY_OCH_UNDERHALLNING,
68
66
  )
69
67
 
70
- # search for cars only
71
- result = api.search_car(
68
+ # search for cars
69
+ api.search_car(
72
70
  "Audi", # query is optional
73
71
  sort_order=CarSortOrder.MILEAGE_ASC,
74
72
  models=[CarModel.AUDI],
75
73
  colors=[CarColor.GULD],
76
74
  price_from=10000,
77
75
  price_to=50000,
78
- transmission=CarTransmission.MANUAL,
76
+ transmissions=[CarTransmission.MANUAL],
77
+ locations=[Location.STOCKHOLM],
78
+ )
79
+
80
+ # search for boats
81
+ from blocket_api import BoatType
82
+
83
+ api.search_boat(
84
+ "Mercury", # query is optional
85
+ types=[BoatType.DAYCRUISER],
79
86
  locations=[Location.STOCKHOLM],
87
+ length_from=10,
88
+ length_to=15,
89
+ price_from=20000,
90
+ price_to=90000,
80
91
  )
92
+
93
+
94
+ # get ad details
95
+ from blocket_api import CarAd, RecommerceAd, BoatAd
96
+
97
+ api.get_ad(RecommerceAd(12345678))
98
+ api.get_ad(CarAd(12345678))
99
+ api.get_ad(BoatAd(12345678))
81
100
  ```
82
101
 
83
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...
@@ -38,25 +35,46 @@ from blocket_api import (
38
35
 
39
36
  api = BlocketAPI()
40
37
 
41
- # will search all of blocket
42
- result = api.search(
43
- "Vinterdäck Audi",
38
+ # search all of blocket
39
+ api.search(
40
+ "Tamagotchi",
44
41
  sort_order=SortOrder.PRICE_ASC,
45
42
  locations=[Location.STOCKHOLM, Location.UPPSALA],
46
- category=Category.FORDONSTILLBEHOR,
43
+ category=Category.FRITID_HOBBY_OCH_UNDERHALLNING,
47
44
  )
48
45
 
49
- # search for cars only
50
- result = api.search_car(
46
+ # search for cars
47
+ api.search_car(
51
48
  "Audi", # query is optional
52
49
  sort_order=CarSortOrder.MILEAGE_ASC,
53
50
  models=[CarModel.AUDI],
54
51
  colors=[CarColor.GULD],
55
52
  price_from=10000,
56
53
  price_to=50000,
57
- transmission=CarTransmission.MANUAL,
54
+ transmissions=[CarTransmission.MANUAL],
55
+ locations=[Location.STOCKHOLM],
56
+ )
57
+
58
+ # search for boats
59
+ from blocket_api import BoatType
60
+
61
+ api.search_boat(
62
+ "Mercury", # query is optional
63
+ types=[BoatType.DAYCRUISER],
58
64
  locations=[Location.STOCKHOLM],
65
+ length_from=10,
66
+ length_to=15,
67
+ price_from=20000,
68
+ price_to=90000,
59
69
  )
70
+
71
+
72
+ # get ad details
73
+ from blocket_api import CarAd, RecommerceAd, BoatAd
74
+
75
+ api.get_ad(RecommerceAd(12345678))
76
+ api.get_ad(CarAd(12345678))
77
+ api.get_ad(BoatAd(12345678))
60
78
  ```
61
79
 
62
80
  ## 📝 Notes
@@ -1,5 +1,9 @@
1
+ from .ad_parser import BoatAd as BoatAd
2
+ from .ad_parser import CarAd as CarAd
3
+ from .ad_parser import RecommerceAd as RecommerceAd
1
4
  from .blocket import BlocketAPI as BlocketAPI
2
5
  from .blocket import Location as Location
6
+ from .constants import BoatType as BoatType
3
7
  from .constants import CarColor as CarColor
4
8
  from .constants import CarModel as CarModel
5
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
- # Car specific
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"
@@ -48,38 +64,26 @@ class Location(StrEnum):
48
64
  VASTERBOTTEN = "0.300024"
49
65
  VASTERNORRLAND = "0.300022"
50
66
  VASTMANLAND = "0.300019"
51
- VASTRA_GÖTALAND = "0.300014"
67
+ VASTRA_GOTALAND = "0.300014"
52
68
  OREBRO = "0.300018"
53
69
  OSTERGOTLAND = "0.300005"
54
70
 
55
71
 
56
72
  class Category(StrEnum):
57
73
  AFFARSVERKSAMHET = "0.91"
58
- # BIL_OCH_HUSVAGN = None
59
- # BAT = None
60
74
  DJUR_OCH_TILLBEHOR = "0.77"
61
75
  ELEKTRONIK_OCH_VITVAROR = "0.93"
62
- # ENTREPRENAD__OCH_LANTBRUKSMASKINER = None
63
76
  FORDONSTILLBEHOR = "0.90"
64
77
  FRITID_HOBBY_OCH_UNDERHALLNING = "0.86"
65
78
  FORALDRAR_OCH_BARN = "0.68"
66
79
  KLADER_KOSMETIKA_OCH_ACCESSOARER = "0.71"
67
80
  KONST_OCH_ANTIKT = "0.76"
68
- # MOTORCYKLAR = None
69
81
  MOBLER_OCH_INREDNING = "0.78"
70
82
  SPORT_OCH_FRITID = "0.69"
71
83
  TRADGARD_OCH_RENOVERING = "0.67"
72
84
 
73
85
 
74
86
  class SubCategory(StrEnum):
75
- BYGGMATERIAL_OCH_RENOVERING = "1.67.3899"
76
- BILDELAR_OCH_TILLBEHOR = "1.90.82"
77
- CYKEL = "1.69.3963"
78
- JAKT_FISKE_OCH_CAMPING = "1.69.3964"
79
- TRADGARD_OCH_UTEMILJO = "1.67.3901"
80
- MC_UTRUSTNING_OCH_RESERVDELAR = "1.90.20"
81
- HUSVAGNS_OCH_HUSBILSDELAR = "1.90.70"
82
- VERKTYG = "1.67.5219"
83
87
  BUTIK_OCH_DETALJHANDEL = "1.91.3108"
84
88
  CONTAINRAR_OCH_BARACKER = "1.91.3111"
85
89
  DOMANER_OCH_SAJTER = "1.91.3106"
@@ -91,6 +95,8 @@ class SubCategory(StrEnum):
91
95
  SCEN = "1.91.3110"
92
96
  STORKOK_OCH_RESTAURANG = "1.91.3103"
93
97
  VERKSTAD_BYGG_OCH_KONSTRUKTION = "1.91.3102"
98
+ BILDELAR_OCH_TILLBEHOR = "1.90.82"
99
+ HUSVAGNS_OCH_HUSBILSDELAR = "1.90.70"
94
100
  SLAP_OCH_TRAILER = "1.90.80"
95
101
  BATDELAR_OCH_TILLBEHOR = "1.90.30"
96
102
  AKVARIUM = "1.77.3976"
@@ -119,7 +125,79 @@ class SubCategory(StrEnum):
119
125
  TV_SPEL_OCH_SPELKONSOLER = "1.93.3905"
120
126
  VITVAROR = "1.93.3907"
121
127
  ATV_RESERVDELAR = "1.90.3975"
122
- OVRIGT = "1.90.3974"
128
+ MC_UTRUSTNING_OCH_RESERVDELAR = "1.90.20"
129
+ BILJETTER_OCH_RESOR = "1.86.7735"
130
+ BOCKER_OCH_TIDNINGAR = "1.86.5209"
131
+ HANTVERK = "1.86.7734"
132
+ MAT_OCH_DRYCK = "1.86.3972"
133
+ MODELLER_OCH_BYGGSATSER = "1.86.7732"
134
+ MUSIK_OCH_FILM = "1.86.3922"
135
+ MUSIKINSTRUMENT = "1.86.92"
136
+ RADIOSTYRDA_ENHETER = "1.86.7733"
137
+ SAMLAROBJEKT = "1.86.285"
138
+ SALLSKAPS_OCH_BRADSPEL = "1.86.5203"
139
+ BARNBOCKER = "1.68.8369"
140
+ BARNKLADER = "1.68.3913"
141
+ BARNMOBLER = "1.68.3916"
142
+ BARNSKOR = "1.68.3915"
143
+ BARNTILLBEHOR_OCH_SAKERHET = "1.68.3918"
144
+ BARNVAGNAR = "1.68.3914"
145
+ BILBARNSTOLAR_OCH_BABYSKYDD = "1.68.3911"
146
+ GRAVIDKLADER = "1.68.3948"
147
+ INREDNING_TILL_BARNRUM = "1.68.9445"
148
+ LEKSAKER = "1.68.3912"
149
+ ACCESSOARER = "1.71.9481"
150
+ DAMKLADER = "1.71.3941"
151
+ GLASOGON_OCH_SOLGLASOGON = "1.71.8344"
152
+ HERRKLADER = "1.71.3950"
153
+ HUD_HAR_OCH_KROPPSVARD = "1.71.8280"
154
+ KLOCKOR_OCH_ARMBANDSUR = "1.71.3945"
155
+ KOSMETIK = "1.71.8282"
156
+ MASKERADKLADER = "1.71.8349"
157
+ SKOR = "1.71.3949"
158
+ SMYCKEN_OCH_SMYCKESFORVARING = "1.71.7748"
159
+ VASKOR_OCH_PLANBOCKER = "1.71.3946"
160
+ ANTIKA_MOBLER = "1.76.5178"
161
+ KERAMIK_PORSLIN_OCH_GLAS = "1.76.5176"
162
+ KONST = "1.76.5177"
163
+ SILVERFOREMAL_OCH_SILVERBESTICK = "1.76.5179"
164
+ OVRIGA_ANTIKVITETER = "1.76.5175"
165
+ BORD_OCH_STOLAR = "1.78.5196"
166
+ DEKORATION_OCH_PRYDNADER = "1.78.5222"
167
+ GARDEROBER_OCH_FORVARING = "1.78.5198"
168
+ HYLLOR_OCH_BYRAER = "1.78.8345"
169
+ KOKSUTRUSTNING_OCH_PORSLIN = "1.78.5223"
170
+ LAMPOR = "1.78.5181"
171
+ MATTOR_OCH_TEXTILIER = "1.78.5180"
172
+ PYNT_TILL_HOGTIDER_OCH_FEST = "1.78.9760"
173
+ SOFFOR_OCH_FATOLJER = "1.78.7756"
174
+ SANGAR_OCH_MADRASSER = "1.78.5197"
175
+ OVRIGA_MOBLER_OCH_INREDNING = "1.78.3971"
176
+ BOLLSPORTER = "1.69.3961"
177
+ CYKEL = "1.69.3963"
178
+ EXTREMSPORT = "1.69.3938"
179
+ GOLF = "1.69.5164"
180
+ JAKT_FISKE_OCH_CAMPING = "1.69.3964"
181
+ KOSTTILLSKOTT = "1.69.8281"
182
+ RULLSKRIDSKOR_ISHOCKEY_OCH_KONSTAKNING = "1.69.8346"
183
+ SKYTTE = "1.69.3965"
184
+ SUPPORTERPRODUKTER = "1.69.3937"
185
+ TRANINGSKLOCKOR_OCH_AKTIVITETSARMBAND = "1.69.3967"
186
+ TRANINGSKLADER_OCH_SKOR = "1.69.3940"
187
+ TRANINGSUTRUSTNING = "1.69.5166"
188
+ VATTENSPORT = "1.69.7738"
189
+ VINTERSPORT = "1.69.3962"
190
+ OVRIGA_SPORTER = "1.69.3966"
191
+ BADRUM_OCH_BASTU = "1.67.7749"
192
+ BYGGMATERIAL_OCH_RENOVERING = "1.67.3899"
193
+ GARAGEDELAR_OCH_TILLBEHOR = "1.67.8348"
194
+ KOKSINREDNING_OCH_KOKSSTOMMAR = "1.67.3900"
195
+ LARM_OCH_SAKERHET = "1.67.8347"
196
+ TRADGARD_OCH_UTEMILJO = "1.67.3901"
197
+ UTRUSTNING_FOR_FRITIDSHUS = "1.67.3968"
198
+ VERKTYG = "1.67.5219"
199
+ VARME_OCH_VENTILATION = "1.67.5218"
200
+ OVRIGT = "1.67.3969"
123
201
 
124
202
 
125
203
  class CarModel(StrEnum):
@@ -286,3 +364,20 @@ class CarColor(IntEnum):
286
364
  class CarTransmission(IntEnum):
287
365
  AUTOMATIC = 2
288
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.91
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
@@ -16,7 +16,8 @@ Requires-Dist: mkdocs>=1.6.1
16
16
  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
- Requires-Dist: ruff>=0.14.5
19
+ Requires-Dist: beautifulsoup4>=4.14.2
20
+ Requires-Dist: ruff>=0.14.6
20
21
  Dynamic: license-file
21
22
 
22
23
  <p align="center">
@@ -31,9 +32,6 @@ BlocketAPI allows users to search [blocket.se](https://blocket.se/) for ads.
31
32
 
32
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.
33
34
 
34
- > [!NOTE]
35
- > 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).
36
-
37
35
  ## 🧑‍💻️ Install
38
36
 
39
37
  Install BlocketAPI via PyPI...
@@ -59,25 +57,46 @@ from blocket_api import (
59
57
 
60
58
  api = BlocketAPI()
61
59
 
62
- # will search all of blocket
63
- result = api.search(
64
- "Vinterdäck Audi",
60
+ # search all of blocket
61
+ api.search(
62
+ "Tamagotchi",
65
63
  sort_order=SortOrder.PRICE_ASC,
66
64
  locations=[Location.STOCKHOLM, Location.UPPSALA],
67
- category=Category.FORDONSTILLBEHOR,
65
+ category=Category.FRITID_HOBBY_OCH_UNDERHALLNING,
68
66
  )
69
67
 
70
- # search for cars only
71
- result = api.search_car(
68
+ # search for cars
69
+ api.search_car(
72
70
  "Audi", # query is optional
73
71
  sort_order=CarSortOrder.MILEAGE_ASC,
74
72
  models=[CarModel.AUDI],
75
73
  colors=[CarColor.GULD],
76
74
  price_from=10000,
77
75
  price_to=50000,
78
- transmission=CarTransmission.MANUAL,
76
+ transmissions=[CarTransmission.MANUAL],
77
+ locations=[Location.STOCKHOLM],
78
+ )
79
+
80
+ # search for boats
81
+ from blocket_api import BoatType
82
+
83
+ api.search_boat(
84
+ "Mercury", # query is optional
85
+ types=[BoatType.DAYCRUISER],
79
86
  locations=[Location.STOCKHOLM],
87
+ length_from=10,
88
+ length_to=15,
89
+ price_from=20000,
90
+ price_to=90000,
80
91
  )
92
+
93
+
94
+ # get ad details
95
+ from blocket_api import CarAd, RecommerceAd, BoatAd
96
+
97
+ api.get_ad(RecommerceAd(12345678))
98
+ api.get_ad(CarAd(12345678))
99
+ api.get_ad(BoatAd(12345678))
81
100
  ```
82
101
 
83
102
  ## 📝 Notes
@@ -2,6 +2,7 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  blocket_api/__init__.py
5
+ blocket_api/ad_parser.py
5
6
  blocket_api/blocket.py
6
7
  blocket_api/constants.py
7
8
  blocket_api.egg-info/PKG-INFO
@@ -8,4 +8,5 @@ mkdocs>=1.6.1
8
8
  mkdocs-material>=9.6.21
9
9
  mkdocs-swagger-ui-tag>=0.7.2
10
10
  websockets>=15.0.1
11
- ruff>=0.14.5
11
+ beautifulsoup4>=4.14.2
12
+ ruff>=0.14.6
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "blocket_api"
3
- version = "0.3.91"
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"
@@ -16,7 +16,8 @@ dependencies = [
16
16
  "mkdocs-material>=9.6.21",
17
17
  "mkdocs-swagger-ui-tag>=0.7.2",
18
18
  "websockets>=15.0.1",
19
- "ruff>=0.14.5",
19
+ "beautifulsoup4>=4.14.2",
20
+ "ruff>=0.14.6",
20
21
  ]
21
22
 
22
23
 
@@ -1,98 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
- from typing import Any
5
-
6
- import httpx
7
-
8
- from .constants import (
9
- HEADERS,
10
- SITE_URL,
11
- CarColor,
12
- CarModel,
13
- CarSortOrder,
14
- Category,
15
- Location,
16
- SortOrder,
17
- SubCategory,
18
- )
19
-
20
-
21
- @dataclass(frozen=True)
22
- class QueryParam:
23
- name: str
24
- value: str | int
25
-
26
-
27
- def _request(
28
- *,
29
- url: str,
30
- params: list[QueryParam],
31
- ) -> Any:
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.json()
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
- params = [
57
- QueryParam("q", query),
58
- QueryParam("sort", sort_order.value),
59
- *[QueryParam("location", location.value) for location in locations],
60
- *([QueryParam("category", category.value)] if category else []),
61
- *([QueryParam("sub_category", sub_category.value)] if sub_category else []),
62
- ]
63
-
64
- return _request(url=url, params=params)
65
-
66
- def search_car(
67
- self,
68
- query: str | None = None,
69
- *,
70
- sort_order: CarSortOrder = CarSortOrder.RELEVANCE,
71
- locations: list[Location] = [],
72
- models: list[CarModel] = [],
73
- price_from: int | None = None,
74
- price_to: int | None = None,
75
- year_from: int | None = None,
76
- year_to: int | None = None,
77
- milage_from: int | None = None,
78
- milage_to: int | None = None,
79
- colors: list[CarColor] = [],
80
- transmission: int | None = None,
81
- ) -> Any:
82
- url = f"{SITE_URL}/mobility/search/api/search/SEARCH_ID_CAR_USED"
83
- params = [
84
- *([QueryParam("q", query)] if query else []),
85
- QueryParam("sort", sort_order.value),
86
- *[QueryParam("location", location.value) for location in locations],
87
- *[QueryParam("make", model.value) for model in models],
88
- *([QueryParam("price_from", price_from)] if price_from else []),
89
- *([QueryParam("price_to", price_to)] if price_to else []),
90
- *([QueryParam("year_from", year_from)] if year_from else []),
91
- *([QueryParam("year_to", year_to)] if year_to else []),
92
- *([QueryParam("milage_from", milage_from)] if milage_from else []),
93
- *([QueryParam("milage_to", milage_to)] if milage_to else []),
94
- *[QueryParam("exterior_colour", color.value) for color in colors],
95
- *([QueryParam("transmission", transmission)] if transmission else []),
96
- ]
97
-
98
- return _request(url=url, params=params)
File without changes
File without changes