blocket-api 0.3.9__tar.gz → 0.3.92__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.9
3
+ Version: 0.3.92
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,6 +16,7 @@ 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: beautifulsoup4>=4.14.2
19
20
  Requires-Dist: ruff>=0.14.5
20
21
  Dynamic: license-file
21
22
 
@@ -47,16 +48,43 @@ or use [blocket-api.se](https://blocket-api.se) without installing anything!
47
48
  ## 💁‍♀️ Usage
48
49
 
49
50
  ```py
50
- from blocket_api import BlocketAPI, Category, Location, SortOrder
51
+ from blocket_api import (
52
+ BlocketAPI,
53
+ Category,
54
+ CarColor,
55
+ CarModel,
56
+ CarSortOrder,
57
+ CarTransmission,
58
+ Location,
59
+ )
51
60
 
52
61
  api = BlocketAPI()
53
62
 
54
- result = api.search(
63
+ # search all of blocket
64
+ api.search(
55
65
  "Vinterdäck Audi",
56
66
  sort_order=SortOrder.PRICE_ASC,
57
67
  locations=[Location.STOCKHOLM, Location.UPPSALA],
58
68
  category=Category.FORDONSTILLBEHOR,
59
69
  )
70
+
71
+ # search for cars only
72
+ api.search_car(
73
+ "Audi", # query is optional
74
+ sort_order=CarSortOrder.MILEAGE_ASC,
75
+ models=[CarModel.AUDI],
76
+ colors=[CarColor.GULD],
77
+ price_from=10000,
78
+ price_to=50000,
79
+ transmissions=[CarTransmission.MANUAL],
80
+ locations=[Location.STOCKHOLM],
81
+ )
82
+
83
+ # Get a recommerce or car ad
84
+ from blocket_api import CarAd, RecommerceAd
85
+
86
+ api.get_ad(RecommerceAd(12345678))
87
+ api.get_ad(CarAd(12345678))
60
88
  ```
61
89
 
62
90
  ## 📝 Notes
@@ -26,16 +26,43 @@ or use [blocket-api.se](https://blocket-api.se) without installing anything!
26
26
  ## 💁‍♀️ Usage
27
27
 
28
28
  ```py
29
- from blocket_api import BlocketAPI, Category, Location, SortOrder
29
+ from blocket_api import (
30
+ BlocketAPI,
31
+ Category,
32
+ CarColor,
33
+ CarModel,
34
+ CarSortOrder,
35
+ CarTransmission,
36
+ Location,
37
+ )
30
38
 
31
39
  api = BlocketAPI()
32
40
 
33
- result = api.search(
41
+ # search all of blocket
42
+ api.search(
34
43
  "Vinterdäck Audi",
35
44
  sort_order=SortOrder.PRICE_ASC,
36
45
  locations=[Location.STOCKHOLM, Location.UPPSALA],
37
46
  category=Category.FORDONSTILLBEHOR,
38
47
  )
48
+
49
+ # search for cars only
50
+ api.search_car(
51
+ "Audi", # query is optional
52
+ sort_order=CarSortOrder.MILEAGE_ASC,
53
+ models=[CarModel.AUDI],
54
+ colors=[CarColor.GULD],
55
+ price_from=10000,
56
+ price_to=50000,
57
+ transmissions=[CarTransmission.MANUAL],
58
+ locations=[Location.STOCKHOLM],
59
+ )
60
+
61
+ # Get a recommerce or car ad
62
+ from blocket_api import CarAd, RecommerceAd
63
+
64
+ api.get_ad(RecommerceAd(12345678))
65
+ api.get_ad(CarAd(12345678))
39
66
  ```
40
67
 
41
68
  ## 📝 Notes
@@ -0,0 +1,11 @@
1
+ from .ad_parser import CarAd as CarAd
2
+ from .ad_parser import RecommerceAd as RecommerceAd
3
+ from .blocket import BlocketAPI as BlocketAPI
4
+ from .blocket import Location as Location
5
+ from .constants import CarColor as CarColor
6
+ from .constants import CarModel as CarModel
7
+ from .constants import CarSortOrder as CarSortOrder
8
+ from .constants import CarTransmission as CarTransmission
9
+ from .constants import Category as Category
10
+ from .constants import SortOrder as SortOrder
11
+ from .constants import SubCategory as SubCategory
@@ -0,0 +1,182 @@
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
@@ -0,0 +1,105 @@
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)
@@ -0,0 +1,350 @@
1
+ from enum import IntEnum, StrEnum
2
+
3
+ SITE_URL = "https://www.blocket.se"
4
+ HEADERS = {
5
+ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0"
6
+ }
7
+
8
+
9
+ class SortOrder(StrEnum):
10
+ RELEVANCE = "RELEVANCE"
11
+ PRICE_DESC = "PRICE_DESC"
12
+ PRICE_ASC = "PRICE_ASC"
13
+ PUBLISHED_DESC = "PUBLISHED_DESC"
14
+ PUBLISHED_ASC = "PUBLISHED_ASC"
15
+
16
+
17
+ class CarSortOrder(StrEnum):
18
+ RELEVANCE = "RELEVANCE"
19
+ PRICE_DESC = "PRICE_DESC"
20
+ PRICE_ASC = "PRICE_ASC"
21
+ PUBLISHED_DESC = "PUBLISHED_DESC"
22
+ PUBLISHED_ASC = "PUBLISHED_ASC"
23
+
24
+ # Car specific
25
+ MILEAGE_DESC = "MILEAGE_DESC"
26
+ MILEAGE_ASC = "MILEAGE_ASC"
27
+ MODEL = "MODEL"
28
+ YEAR_DESC = "YEAR_DESC"
29
+ YEAR_ASC = "YEAR_ASC"
30
+
31
+
32
+ class Location(StrEnum):
33
+ BLEKINGE = "0.300010"
34
+ DALARNA = "0.300020"
35
+ GOTLAND = "0.300009"
36
+ GAVLEBORG = "0.300021"
37
+ HALLAND = "0.300013"
38
+ JAMTLAND = "0.300023"
39
+ JONKOPING = "0.300006"
40
+ KALMAR = "0.300008"
41
+ KRONOBERG = "0.300007"
42
+ NORRBOTTEN = "0.300025"
43
+ SKANE = "0.300012"
44
+ STOCKHOLM = "0.300001"
45
+ SODERMANLAND = "0.300004"
46
+ UPPSALA = "0.300003"
47
+ VARMLAND = "0.300017"
48
+ VASTERBOTTEN = "0.300024"
49
+ VASTERNORRLAND = "0.300022"
50
+ VASTMANLAND = "0.300019"
51
+ VASTRA_GOTALAND = "0.300014"
52
+ OREBRO = "0.300018"
53
+ OSTERGOTLAND = "0.300005"
54
+
55
+
56
+ class Category(StrEnum):
57
+ AFFARSVERKSAMHET = "0.91"
58
+ DJUR_OCH_TILLBEHOR = "0.77"
59
+ ELEKTRONIK_OCH_VITVAROR = "0.93"
60
+ FORDONSTILLBEHOR = "0.90"
61
+ FRITID_HOBBY_OCH_UNDERHALLNING = "0.86"
62
+ FORALDRAR_OCH_BARN = "0.68"
63
+ KLADER_KOSMETIKA_OCH_ACCESSOARER = "0.71"
64
+ KONST_OCH_ANTIKT = "0.76"
65
+ MOBLER_OCH_INREDNING = "0.78"
66
+ SPORT_OCH_FRITID = "0.69"
67
+ TRADGARD_OCH_RENOVERING = "0.67"
68
+
69
+
70
+ class SubCategory(StrEnum):
71
+ BUTIK_OCH_DETALJHANDEL = "1.91.3108"
72
+ CONTAINRAR_OCH_BARACKER = "1.91.3111"
73
+ DOMANER_OCH_SAJTER = "1.91.3106"
74
+ HALSA_OCH_FORSTA_HJALPEN = "1.91.8343"
75
+ JORDBRUK = "1.91.3114"
76
+ KONTORSUTRUSTNING_OCH_INREDNING = "1.91.3105"
77
+ LAST_OCH_TRANSPORT = "1.91.3113"
78
+ MASKINUTRUSTNING_OCH_RESERVDELAR = "1.91.3112"
79
+ SCEN = "1.91.3110"
80
+ STORKOK_OCH_RESTAURANG = "1.91.3103"
81
+ VERKSTAD_BYGG_OCH_KONSTRUKTION = "1.91.3102"
82
+ BILDELAR_OCH_TILLBEHOR = "1.90.82"
83
+ HUSVAGNS_OCH_HUSBILSDELAR = "1.90.70"
84
+ SLAP_OCH_TRAILER = "1.90.80"
85
+ BATDELAR_OCH_TILLBEHOR = "1.90.30"
86
+ AKVARIUM = "1.77.3976"
87
+ BURAR = "1.77.3977"
88
+ FISKAR = "1.77.5206"
89
+ FODER_DJURVARD_KENNLAR_OCH_STALL = "1.77.5186"
90
+ FAGLAR = "1.77.5205"
91
+ GNAGARE_OCH_KANINER = "1.77.5207"
92
+ HUNDAR = "1.77.5192"
93
+ HUNDTILLBEHOR = "1.77.5193"
94
+ HAST_OCH_RIDUTRUSTNING = "1.77.5195"
95
+ HASTAR = "1.77.5190"
96
+ KATTER = "1.77.5191"
97
+ KATTILLBEHOR = "1.77.5194"
98
+ LANTBRUKSDJUR = "1.77.9439"
99
+ REPTILER = "1.77.5210"
100
+ SPINDLAR_OCH_INSEKTER = "1.77.5208"
101
+ OVRIGA_DJUR = "1.77.5183"
102
+ OVRIGA_DJURTILLBEHOR = "1.77.5185"
103
+ DATORER = "1.93.3215"
104
+ FOTO_OCH_VIDEO = "1.93.3904"
105
+ HUSHALLSAPPARATER = "1.93.3216"
106
+ LJUD_OCH_BILD = "1.93.3906"
107
+ PERSONVARD = "1.93.9809"
108
+ TELEFONER_OCH_TILLBEHOR = "1.93.3217"
109
+ TV_SPEL_OCH_SPELKONSOLER = "1.93.3905"
110
+ VITVAROR = "1.93.3907"
111
+ ATV_RESERVDELAR = "1.90.3975"
112
+ MC_UTRUSTNING_OCH_RESERVDELAR = "1.90.20"
113
+ BILJETTER_OCH_RESOR = "1.86.7735"
114
+ BOCKER_OCH_TIDNINGAR = "1.86.5209"
115
+ HANTVERK = "1.86.7734"
116
+ MAT_OCH_DRYCK = "1.86.3972"
117
+ MODELLER_OCH_BYGGSATSER = "1.86.7732"
118
+ MUSIK_OCH_FILM = "1.86.3922"
119
+ MUSIKINSTRUMENT = "1.86.92"
120
+ RADIOSTYRDA_ENHETER = "1.86.7733"
121
+ SAMLAROBJEKT = "1.86.285"
122
+ SALLSKAPS_OCH_BRADSPEL = "1.86.5203"
123
+ BARNBOCKER = "1.68.8369"
124
+ BARNKLADER = "1.68.3913"
125
+ BARNMOBLER = "1.68.3916"
126
+ BARNSKOR = "1.68.3915"
127
+ BARNTILLBEHOR_OCH_SAKERHET = "1.68.3918"
128
+ BARNVAGNAR = "1.68.3914"
129
+ BILBARNSTOLAR_OCH_BABYSKYDD = "1.68.3911"
130
+ GRAVIDKLADER = "1.68.3948"
131
+ INREDNING_TILL_BARNRUM = "1.68.9445"
132
+ LEKSAKER = "1.68.3912"
133
+ ACCESSOARER = "1.71.9481"
134
+ DAMKLADER = "1.71.3941"
135
+ GLASOGON_OCH_SOLGLASOGON = "1.71.8344"
136
+ HERRKLADER = "1.71.3950"
137
+ HUD_HAR_OCH_KROPPSVARD = "1.71.8280"
138
+ KLOCKOR_OCH_ARMBANDSUR = "1.71.3945"
139
+ KOSMETIK = "1.71.8282"
140
+ MASKERADKLADER = "1.71.8349"
141
+ SKOR = "1.71.3949"
142
+ SMYCKEN_OCH_SMYCKESFORVARING = "1.71.7748"
143
+ VASKOR_OCH_PLANBOCKER = "1.71.3946"
144
+ ANTIKA_MOBLER = "1.76.5178"
145
+ KERAMIK_PORSLIN_OCH_GLAS = "1.76.5176"
146
+ KONST = "1.76.5177"
147
+ SILVERFOREMAL_OCH_SILVERBESTICK = "1.76.5179"
148
+ OVRIGA_ANTIKVITETER = "1.76.5175"
149
+ BORD_OCH_STOLAR = "1.78.5196"
150
+ DEKORATION_OCH_PRYDNADER = "1.78.5222"
151
+ GARDEROBER_OCH_FORVARING = "1.78.5198"
152
+ HYLLOR_OCH_BYRAER = "1.78.8345"
153
+ KOKSUTRUSTNING_OCH_PORSLIN = "1.78.5223"
154
+ LAMPOR = "1.78.5181"
155
+ MATTOR_OCH_TEXTILIER = "1.78.5180"
156
+ PYNT_TILL_HOGTIDER_OCH_FEST = "1.78.9760"
157
+ SOFFOR_OCH_FATOLJER = "1.78.7756"
158
+ SANGAR_OCH_MADRASSER = "1.78.5197"
159
+ OVRIGA_MOBLER_OCH_INREDNING = "1.78.3971"
160
+ BOLLSPORTER = "1.69.3961"
161
+ CYKEL = "1.69.3963"
162
+ EXTREMSPORT = "1.69.3938"
163
+ GOLF = "1.69.5164"
164
+ JAKT_FISKE_OCH_CAMPING = "1.69.3964"
165
+ KOSTTILLSKOTT = "1.69.8281"
166
+ RULLSKRIDSKOR_ISHOCKEY_OCH_KONSTAKNING = "1.69.8346"
167
+ SKYTTE = "1.69.3965"
168
+ SUPPORTERPRODUKTER = "1.69.3937"
169
+ TRANINGSKLOCKOR_OCH_AKTIVITETSARMBAND = "1.69.3967"
170
+ TRANINGSKLADER_OCH_SKOR = "1.69.3940"
171
+ TRANINGSUTRUSTNING = "1.69.5166"
172
+ VATTENSPORT = "1.69.7738"
173
+ VINTERSPORT = "1.69.3962"
174
+ OVRIGA_SPORTER = "1.69.3966"
175
+ BADRUM_OCH_BASTU = "1.67.7749"
176
+ BYGGMATERIAL_OCH_RENOVERING = "1.67.3899"
177
+ GARAGEDELAR_OCH_TILLBEHOR = "1.67.8348"
178
+ KOKSINREDNING_OCH_KOKSSTOMMAR = "1.67.3900"
179
+ LARM_OCH_SAKERHET = "1.67.8347"
180
+ TRADGARD_OCH_UTEMILJO = "1.67.3901"
181
+ UTRUSTNING_FOR_FRITIDSHUS = "1.67.3968"
182
+ VERKTYG = "1.67.5219"
183
+ VARME_OCH_VENTILATION = "1.67.5218"
184
+ OVRIGT = "1.67.3969"
185
+
186
+
187
+ class CarModel(StrEnum):
188
+ ABARTH = "0.8093"
189
+ AC = "0.200673"
190
+ ACURA = "0.200674"
191
+ AIWAYS = "0.200681"
192
+ ALFA_ROMEO = "0.3233"
193
+ ALPINA = "0.8092"
194
+ AMC = "0.8103"
195
+ ARIEL = "0.8110"
196
+ ARMSTRONG_SIDDELEY = "0.1156"
197
+ ASTON_MARTIN = "0.6733"
198
+ AUDI = "0.744"
199
+ AUSTIN = "0.8076"
200
+ AUSTIN_HEALEY = "0.200688"
201
+ AUTO_UNION = "0.200689"
202
+ AUTOBIANCHI = "0.200690"
203
+ BEDFORD = "0.200693"
204
+ BENTLEY = "0.7166"
205
+ BMW = "0.749"
206
+ BUGATTI = "0.8111"
207
+ BUICK = "0.750"
208
+ BYD = "0.8101"
209
+ CADILLAC = "0.752"
210
+ CATERHAM = "0.200704"
211
+ CHEVROLET = "0.753"
212
+ CHRYSLER = "0.754"
213
+ CITROEN = "0.757"
214
+ CUPRA = "0.8106"
215
+ DACIA = "0.8079"
216
+ DAEWOO = "0.760"
217
+ DAF = "0.8090"
218
+ DAIHATSU = "0.762"
219
+ DAIMLER = "0.200711"
220
+ DATSUN = "0.8089"
221
+ DE_TOMASO = "0.8069"
222
+ DELOREAN = "0.8085"
223
+ DESOTO = "0.200715"
224
+ DFSK = "0.2174"
225
+ DKW = "0.200718"
226
+ DODGE = "0.764"
227
+ DS = "0.8091"
228
+ EDSEL = "0.200723"
229
+ ERSKINE = "0.200726"
230
+ EXCALIBUR = "0.200727"
231
+ FERRARI = "0.2999"
232
+ FIAT = "0.766"
233
+ FISKER = "0.8073"
234
+ FORD = "0.767"
235
+ FORDSON = "0.200730"
236
+ GAZ = "0.200734"
237
+ GINETTA = "0.200738"
238
+ GMC = "0.7547"
239
+ HEINKEL = "0.200745"
240
+ HILLMAN = "0.200746"
241
+ HOLDEN = "0.200747"
242
+ HONDA = "0.771"
243
+ HONGQI = "0.8107"
244
+ HUDSON = "0.200748"
245
+ HUMBER = "0.200749"
246
+ HUMMER = "0.7672"
247
+ HYUNDAI = "0.772"
248
+ INEOS = "0.2000665"
249
+ INFINITI = "0.8065"
250
+ INTERNATIONAL = "0.1160"
251
+ ISUZU = "0.7179"
252
+ IVECO = "0.7280"
253
+ JAC = "0.8114"
254
+ JAGUAR = "0.775"
255
+ JEEP = "0.776"
256
+ JENSEN = "0.774"
257
+ KAISER_JEEP = "0.1162"
258
+ KGM = "0.2000649"
259
+ KIA = "0.777"
260
+ KTM = "0.200761"
261
+ LADA = "0.779"
262
+ LAMBORGHINI = "0.6731"
263
+ LANCIA = "0.780"
264
+ LAND_ROVER = "0.781"
265
+ LEVC = "0.200764"
266
+ LEXUS = "0.782"
267
+ LEYLAND = "0.200765"
268
+ LINCOLN = "0.7153"
269
+ LOTUS = "0.7191"
270
+ LYNK_CO = "0.200769"
271
+ MAN = "0.8097"
272
+ MASERATI = "0.3001"
273
+ MAXUS = "0.8096"
274
+ MAZDA = "0.784"
275
+ MCLAREN = "0.8087"
276
+ MERCEDES_BENZ = "0.785"
277
+ MERCURY = "0.7554"
278
+ MESSERSCHMITT = "0.200774"
279
+ MG = "0.786"
280
+ MINI = "0.7147"
281
+ MINI_MARCOS = "0.200775"
282
+ MITSUBISHI = "0.787"
283
+ MORGAN = "0.788"
284
+ MORRIS = "0.789"
285
+ NIO = "0.8109"
286
+ NISSAN = "0.792"
287
+ OLDSMOBILE = "0.794"
288
+ OPEL = "0.795"
289
+ PACKARD = "0.8077"
290
+ PEUGEOT = "0.796"
291
+ PLYMOUTH = "0.797"
292
+ POLESTAR = "0.8102"
293
+ PONTIAC = "0.800"
294
+ PORSCHE = "0.801"
295
+ PRO_SPORT = "0.200792"
296
+ RADICAL = "0.8088"
297
+ RAM = "0.8100"
298
+ RENAULT = "0.804"
299
+ ROLLS_ROYCE = "0.7170"
300
+ ROVER = "0.805"
301
+ SAAB = "0.806"
302
+ SCION = "0.822"
303
+ SEAT = "0.807"
304
+ SERES = "0.8108"
305
+ SHELBY = "0.1142"
306
+ SIMCA = "0.200807"
307
+ SKODA = "0.808"
308
+ SMART = "0.7137"
309
+ SSANGYONG = "0.7190"
310
+ STANDARD = "0.200812"
311
+ STUDEBAKER = "0.200815"
312
+ SUBARU = "0.810"
313
+ SUZUKI = "0.811"
314
+ TESLA = "0.8078"
315
+ TOYOTA = "0.813"
316
+ TRABANT = "0.200824"
317
+ TRIUMPH = "0.814"
318
+ TVR = "0.820"
319
+ VAUXHALL = "0.200827"
320
+ VOLKSWAGEN = "0.817"
321
+ VOLVO = "0.818"
322
+ WILLYS = "0.200834"
323
+ XPENG = "0.8104"
324
+ ZEEKR = "0.200841"
325
+ ZIMMER = "0.200844"
326
+ OVRIGA = "0.2252"
327
+
328
+
329
+ class CarColor(IntEnum):
330
+ BEIGE = 1
331
+ BLA = 2
332
+ BRONS = 3
333
+ BRUN = 4
334
+ GRA = 6
335
+ GRON = 5
336
+ GUL = 7
337
+ GULD = 8
338
+ VIT = 9
339
+ LILA = 10
340
+ ORANGE = 11
341
+ ROSA = 12
342
+ ROD = 13
343
+ SILVER = 15
344
+ SVART = 14
345
+ TURKOS = 16
346
+
347
+
348
+ class CarTransmission(IntEnum):
349
+ AUTOMATIC = 2
350
+ MANUAL = 1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: blocket_api
3
- Version: 0.3.9
3
+ Version: 0.3.92
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,6 +16,7 @@ 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: beautifulsoup4>=4.14.2
19
20
  Requires-Dist: ruff>=0.14.5
20
21
  Dynamic: license-file
21
22
 
@@ -47,16 +48,43 @@ or use [blocket-api.se](https://blocket-api.se) without installing anything!
47
48
  ## 💁‍♀️ Usage
48
49
 
49
50
  ```py
50
- from blocket_api import BlocketAPI, Category, Location, SortOrder
51
+ from blocket_api import (
52
+ BlocketAPI,
53
+ Category,
54
+ CarColor,
55
+ CarModel,
56
+ CarSortOrder,
57
+ CarTransmission,
58
+ Location,
59
+ )
51
60
 
52
61
  api = BlocketAPI()
53
62
 
54
- result = api.search(
63
+ # search all of blocket
64
+ api.search(
55
65
  "Vinterdäck Audi",
56
66
  sort_order=SortOrder.PRICE_ASC,
57
67
  locations=[Location.STOCKHOLM, Location.UPPSALA],
58
68
  category=Category.FORDONSTILLBEHOR,
59
69
  )
70
+
71
+ # search for cars only
72
+ api.search_car(
73
+ "Audi", # query is optional
74
+ sort_order=CarSortOrder.MILEAGE_ASC,
75
+ models=[CarModel.AUDI],
76
+ colors=[CarColor.GULD],
77
+ price_from=10000,
78
+ price_to=50000,
79
+ transmissions=[CarTransmission.MANUAL],
80
+ locations=[Location.STOCKHOLM],
81
+ )
82
+
83
+ # Get a recommerce or car ad
84
+ from blocket_api import CarAd, RecommerceAd
85
+
86
+ api.get_ad(RecommerceAd(12345678))
87
+ api.get_ad(CarAd(12345678))
60
88
  ```
61
89
 
62
90
  ## 📝 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
+ beautifulsoup4>=4.14.2
11
12
  ruff>=0.14.5
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "blocket_api"
3
- version = "0.3.9"
3
+ version = "0.3.92"
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,6 +16,7 @@ dependencies = [
16
16
  "mkdocs-material>=9.6.21",
17
17
  "mkdocs-swagger-ui-tag>=0.7.2",
18
18
  "websockets>=15.0.1",
19
+ "beautifulsoup4>=4.14.2",
19
20
  "ruff>=0.14.5",
20
21
  ]
21
22
 
@@ -1,5 +0,0 @@
1
- from .blocket import BlocketAPI as BlocketAPI
2
- from .blocket import Location as Location
3
- from .constants import Category as Category
4
- from .constants import SortOrder as SortOrder
5
- from .constants import SubCategory as SubCategory
@@ -1,54 +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 HEADERS, SITE_URL, Category, Location, SortOrder, SubCategory
9
-
10
-
11
- @dataclass(frozen=True)
12
- class QueryParam:
13
- name: str
14
- value: str
15
-
16
-
17
- def _request(
18
- *,
19
- url: str,
20
- params: list[QueryParam],
21
- ) -> Any:
22
- response = httpx.get(
23
- url,
24
- headers=HEADERS,
25
- params=[(param.name, param.value) for param in params],
26
- )
27
- response.raise_for_status()
28
- return response.json()
29
-
30
-
31
- @dataclass(frozen=True)
32
- class BlocketAPI:
33
- def search(
34
- self,
35
- query: str,
36
- *,
37
- sort_order: SortOrder = SortOrder.RELEVANCE,
38
- locations: list[Location] = [],
39
- category: Category | None = None,
40
- sub_category: SubCategory | None = None,
41
- ) -> Any:
42
- if category and sub_category:
43
- raise AssertionError("Cannot specify both category and sub_categories")
44
-
45
- url = f"{SITE_URL}/recommerce-search-page/api/search/SEARCH_ID_BAP_COMMON"
46
- params = [
47
- QueryParam("q", query),
48
- QueryParam("sort", sort_order.value),
49
- *[QueryParam("location", location.value) for location in locations],
50
- *([QueryParam("category", category.value)] if category else []),
51
- *([QueryParam("sub_category", sub_category.value)] if sub_category else []),
52
- ]
53
-
54
- return _request(url=url, params=params)
@@ -1,107 +0,0 @@
1
- from enum import StrEnum
2
-
3
- SITE_URL = "https://www.blocket.se"
4
- HEADERS = {
5
- "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0"
6
- }
7
-
8
-
9
- class SortOrder(StrEnum):
10
- RELEVANCE = "RELEVANCE"
11
- PRICE_DESC = "PRICE_DESC"
12
- PRICE_ASC = "PRICE_ASC"
13
- PUBLISHED_DESC = "PUBLISHED_DESC"
14
- PUBLISHED_ASC = "PUBLISHED_ASC"
15
-
16
-
17
- class Location(StrEnum):
18
- BLEKINGE = "0.300010"
19
- DALARNA = "0.300020"
20
- GOTLAND = "0.300009"
21
- GAVLEBORG = "0.300021"
22
- HALLAND = "0.300013"
23
- JAMTLAND = "0.300023"
24
- JONKOPING = "0.300006"
25
- KALMAR = "0.300008"
26
- KRONOBERG = "0.300007"
27
- NORRBOTTEN = "0.300025"
28
- SKANE = "0.300012"
29
- STOCKHOLM = "0.300001"
30
- SODERMANLAND = "0.300004"
31
- UPPSALA = "0.300003"
32
- VARMLAND = "0.300017"
33
- VASTERBOTTEN = "0.300024"
34
- VASTERNORRLAND = "0.300022"
35
- VASTMANLAND = "0.300019"
36
- VASTRA_GÖTALAND = "0.300014"
37
- OREBRO = "0.300018"
38
- OSTERGOTLAND = "0.300005"
39
-
40
-
41
- class Category(StrEnum):
42
- AFFARSVERKSAMHET = "0.91"
43
- # BIL_OCH_HUSVAGN = None
44
- # BAT = None
45
- DJUR_OCH_TILLBEHOR = "0.77"
46
- ELEKTRONIK_OCH_VITVAROR = "0.93"
47
- # ENTREPRENAD__OCH_LANTBRUKSMASKINER = None
48
- FORDONSTILLBEHOR = "0.90"
49
- FRITID_HOBBY_OCH_UNDERHALLNING = "0.86"
50
- FORALDRAR_OCH_BARN = "0.68"
51
- KLADER_KOSMETIKA_OCH_ACCESSOARER = "0.71"
52
- KONST_OCH_ANTIKT = "0.76"
53
- # MOTORCYKLAR = None
54
- MOBLER_OCH_INREDNING = "0.78"
55
- SPORT_OCH_FRITID = "0.69"
56
- TRADGARD_OCH_RENOVERING = "0.67"
57
-
58
-
59
- class SubCategory(StrEnum):
60
- BYGGMATERIAL_OCH_RENOVERING = "1.67.3899"
61
- BILDELAR_OCH_TILLBEHOR = "1.90.82"
62
- CYKEL = "1.69.3963"
63
- JAKT_FISKE_OCH_CAMPING = "1.69.3964"
64
- TRADGARD_OCH_UTEMILJO = "1.67.3901"
65
- MC_UTRUSTNING_OCH_RESERVDELAR = "1.90.20"
66
- HUSVAGNS_OCH_HUSBILSDELAR = "1.90.70"
67
- VERKTYG = "1.67.5219"
68
- BUTIK_OCH_DETALJHANDEL = "1.91.3108"
69
- CONTAINRAR_OCH_BARACKER = "1.91.3111"
70
- DOMANER_OCH_SAJTER = "1.91.3106"
71
- HALSA_OCH_FORSTA_HJALPEN = "1.91.8343"
72
- JORDBRUK = "1.91.3114"
73
- KONTORSUTRUSTNING_OCH_INREDNING = "1.91.3105"
74
- LAST_OCH_TRANSPORT = "1.91.3113"
75
- MASKINUTRUSTNING_OCH_RESERVDELAR = "1.91.3112"
76
- SCEN = "1.91.3110"
77
- STORKOK_OCH_RESTAURANG = "1.91.3103"
78
- VERKSTAD_BYGG_OCH_KONSTRUKTION = "1.91.3102"
79
- SLAP_OCH_TRAILER = "1.90.80"
80
- BATDELAR_OCH_TILLBEHOR = "1.90.30"
81
- AKVARIUM = "1.77.3976"
82
- BURAR = "1.77.3977"
83
- FISKAR = "1.77.5206"
84
- FODER_DJURVARD_KENNLAR_OCH_STALL = "1.77.5186"
85
- FAGLAR = "1.77.5205"
86
- GNAGARE_OCH_KANINER = "1.77.5207"
87
- HUNDAR = "1.77.5192"
88
- HUNDTILLBEHOR = "1.77.5193"
89
- HAST_OCH_RIDUTRUSTNING = "1.77.5195"
90
- HASTAR = "1.77.5190"
91
- KATTER = "1.77.5191"
92
- KATTILLBEHOR = "1.77.5194"
93
- LANTBRUKSDJUR = "1.77.9439"
94
- REPTILER = "1.77.5210"
95
- SPINDLAR_OCH_INSEKTER = "1.77.5208"
96
- OVRIGA_DJUR = "1.77.5183"
97
- OVRIGA_DJURTILLBEHOR = "1.77.5185"
98
- DATORER = "1.93.3215"
99
- FOTO_OCH_VIDEO = "1.93.3904"
100
- HUSHALLSAPPARATER = "1.93.3216"
101
- LJUD_OCH_BILD = "1.93.3906"
102
- PERSONVARD = "1.93.9809"
103
- TELEFONER_OCH_TILLBEHOR = "1.93.3217"
104
- TV_SPEL_OCH_SPELKONSOLER = "1.93.3905"
105
- VITVAROR = "1.93.3907"
106
- ATV_RESERVDELAR = "1.90.3975"
107
- OVRIGT = "1.90.3974"
File without changes
File without changes