blocket-api 0.5.1__tar.gz → 0.5.3__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,44 +1,45 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: blocket_api
3
- Version: 0.5.1
3
+ Version: 0.5.3
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
7
7
  Description-Content-Type: text/markdown
8
8
  License-File: LICENSE
9
9
  Requires-Dist: httpx>=0.28.1
10
- Requires-Dist: pre-commit>=4.5.1
11
- Requires-Dist: pytest>=9.0.2
12
- Requires-Dist: respx>=0.22.0
13
- Requires-Dist: mypy>=1.19.1
14
- Requires-Dist: mkdocs>=1.6.1
15
- Requires-Dist: mkdocs-material>=9.7.1
16
- Requires-Dist: mkdocs-swagger-ui-tag>=0.7.2
17
- Requires-Dist: beautifulsoup4>=4.14.3
18
- Requires-Dist: ruff>=0.15.12
10
+ Requires-Dist: beautifulsoup4>=4.15.0
11
+ Provides-Extra: dev
12
+ Requires-Dist: pre-commit>=4.6.2; extra == "dev"
13
+ Requires-Dist: pytest>=9.1.1; extra == "dev"
14
+ Requires-Dist: pytest-asyncio>=1.4.0; extra == "dev"
15
+ Requires-Dist: respx>=0.23.1; extra == "dev"
16
+ Requires-Dist: mypy>=2.3.1; extra == "dev"
17
+ Requires-Dist: ruff>=0.16.6; extra == "dev"
18
+ Requires-Dist: flask>=3.1.3; extra == "dev"
19
+ Requires-Dist: htpy>=26.5.1; extra == "dev"
20
+ Requires-Dist: gunicorn>=26.2.0; extra == "dev"
19
21
  Dynamic: license-file
20
22
 
21
- <p align="center">
22
- <img src="https://blocket-api.se/blocket-api.png" />
23
- </p>
24
-
25
23
  # BlocketAPI
26
24
 
27
- [![PyPI version](https://img.shields.io/pypi/v/blocket_api?style=for-the-badge)](https://pypi.org/project/blocket_api/) [![License](https://img.shields.io/badge/license-WTFPL-green?style=for-the-badge)](https://github.com/dunderrrrrr/blocket_api/blob/main/LICENSE) ![Python](https://img.shields.io/badge/Python-3.10-blue?style=for-the-badge) ![PyPI - Downloads](https://img.shields.io/pypi/dm/blocket_api?style=for-the-badge&color=%23dbce58)
25
+ [![PyPI version](https://img.shields.io/pypi/v/blocket_api)](https://pypi.org/project/blocket_api/) [![License](https://img.shields.io/badge/license-WTFPL-green)](https://github.com/dunderrrrrr/blocket_api/blob/main/LICENSE) ![Python](https://img.shields.io/badge/Python-3.10-blue) ![PyPI - Downloads](https://static.pepy.tech/personalized-badge/blocket-api?period=monthly&units=INTERNATIONAL_SYSTEM&left_color=GREY&right_color=RED&left_text=downloads%2Fmonth)
28
26
 
29
27
  BlocketAPI allows users to search [blocket.se](https://blocket.se/) for ads.
30
28
 
31
- > 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.
29
+ > 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.
32
30
 
33
31
  ## 🧑‍💻️ Install
34
32
 
35
33
  Install BlocketAPI via PyPI...
36
34
 
37
35
  ```sh
38
- pip install blocket-api
36
+ pip install blocket-api # using pip
37
+ uv add blocket-api # using uv
39
38
  ```
40
39
 
41
- or use [blocket-api.se](https://blocket-api.se) without installing anything!
40
+ ## 📜 Docs
41
+
42
+ The full documentation can be found at [blocket-api.se](https://blocket-api.se/).
42
43
 
43
44
  ## 💁‍♀️ Usage
44
45
 
@@ -119,6 +120,19 @@ api.get_ad(BoatAd(12345678))
119
120
  api.get_ad(McAd(12345678))
120
121
  ```
121
122
 
123
+ ### Async support
124
+
125
+ ```py
126
+ import asyncio
127
+ from blocket_api import AsyncBlocketAPI
128
+
129
+ async def main():
130
+ async with AsyncBlocketAPI() as api:
131
+ results = await api.search("iPhone 15")
132
+
133
+ asyncio.run(main())
134
+ ```
135
+
122
136
  ## 📝 Notes
123
137
 
124
138
  - REST API: https://blocket-api.se
@@ -1,24 +1,23 @@
1
- <p align="center">
2
- <img src="https://blocket-api.se/blocket-api.png" />
3
- </p>
4
-
5
1
  # BlocketAPI
6
2
 
7
- [![PyPI version](https://img.shields.io/pypi/v/blocket_api?style=for-the-badge)](https://pypi.org/project/blocket_api/) [![License](https://img.shields.io/badge/license-WTFPL-green?style=for-the-badge)](https://github.com/dunderrrrrr/blocket_api/blob/main/LICENSE) ![Python](https://img.shields.io/badge/Python-3.10-blue?style=for-the-badge) ![PyPI - Downloads](https://img.shields.io/pypi/dm/blocket_api?style=for-the-badge&color=%23dbce58)
3
+ [![PyPI version](https://img.shields.io/pypi/v/blocket_api)](https://pypi.org/project/blocket_api/) [![License](https://img.shields.io/badge/license-WTFPL-green)](https://github.com/dunderrrrrr/blocket_api/blob/main/LICENSE) ![Python](https://img.shields.io/badge/Python-3.10-blue) ![PyPI - Downloads](https://static.pepy.tech/personalized-badge/blocket-api?period=monthly&units=INTERNATIONAL_SYSTEM&left_color=GREY&right_color=RED&left_text=downloads%2Fmonth)
8
4
 
9
5
  BlocketAPI allows users to search [blocket.se](https://blocket.se/) for ads.
10
6
 
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.
7
+ > 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
8
 
13
9
  ## 🧑‍💻️ Install
14
10
 
15
11
  Install BlocketAPI via PyPI...
16
12
 
17
13
  ```sh
18
- pip install blocket-api
14
+ pip install blocket-api # using pip
15
+ uv add blocket-api # using uv
19
16
  ```
20
17
 
21
- or use [blocket-api.se](https://blocket-api.se) without installing anything!
18
+ ## 📜 Docs
19
+
20
+ The full documentation can be found at [blocket-api.se](https://blocket-api.se/).
22
21
 
23
22
  ## 💁‍♀️ Usage
24
23
 
@@ -99,6 +98,19 @@ api.get_ad(BoatAd(12345678))
99
98
  api.get_ad(McAd(12345678))
100
99
  ```
101
100
 
101
+ ### Async support
102
+
103
+ ```py
104
+ import asyncio
105
+ from blocket_api import AsyncBlocketAPI
106
+
107
+ async def main():
108
+ async with AsyncBlocketAPI() as api:
109
+ results = await api.search("iPhone 15")
110
+
111
+ asyncio.run(main())
112
+ ```
113
+
102
114
  ## 📝 Notes
103
115
 
104
116
  - REST API: https://blocket-api.se
@@ -1,4 +1,5 @@
1
1
  from .ad_parser import BoatAd, CarAd, McAd, RecommerceAd
2
+ from .async_blocket import AsyncBlocketAPI
2
3
  from .blocket import BlocketAPI, Location
3
4
  from .constants import (
4
5
  BoatSortOrder,
@@ -17,23 +18,24 @@ from .constants import (
17
18
  )
18
19
 
19
20
  __all__ = [
21
+ "AsyncBlocketAPI",
20
22
  "BlocketAPI",
21
- "Location",
22
23
  "BoatAd",
23
- "CarAd",
24
24
  "BoatSortOrder",
25
25
  "BoatType",
26
+ "CarAd",
26
27
  "CarColor",
27
28
  "CarModel",
28
29
  "CarSortOrder",
29
30
  "CarTransmission",
30
31
  "CarWheelDrive",
31
32
  "Category",
32
- "SortOrder",
33
- "SubCategory",
34
- "RecommerceAd",
33
+ "Location",
34
+ "McAd",
35
35
  "McModel",
36
36
  "McSortOrder",
37
37
  "McType",
38
- "McAd",
38
+ "RecommerceAd",
39
+ "SortOrder",
40
+ "SubCategory",
39
41
  ]
@@ -0,0 +1,166 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+
5
+ from .constants import (
6
+ BoatType,
7
+ CarColor,
8
+ CarModel,
9
+ CarSortOrder,
10
+ CarTransmission,
11
+ CarWheelDrive,
12
+ Category,
13
+ Location,
14
+ McModel,
15
+ McSortOrder,
16
+ McType,
17
+ SortOrder,
18
+ SubCategory,
19
+ )
20
+
21
+ _ParamValue = str | int | float | bool | None
22
+
23
+
24
+ def _build_params(
25
+ defaults: dict[str, _ParamValue],
26
+ extras: Sequence[tuple[str, _ParamValue]],
27
+ ) -> list[tuple[str, _ParamValue]]:
28
+ params: list[tuple[str, _ParamValue]] = [
29
+ (k, v) for k, v in defaults.items() if v is not None
30
+ ]
31
+ params.extend(extras)
32
+ return params
33
+
34
+
35
+ def build_search_params(
36
+ query: str,
37
+ *,
38
+ page: int = 1,
39
+ sort_order: SortOrder = SortOrder.RELEVANCE,
40
+ locations: list[Location] = [],
41
+ category: Category | None = None,
42
+ sub_category: SubCategory | None = None,
43
+ ) -> list[tuple[str, _ParamValue]]:
44
+ if category and sub_category:
45
+ raise AssertionError("Cannot specify both category and sub_categories")
46
+
47
+ defaults: dict[str, _ParamValue] = {
48
+ "q": query,
49
+ "page": page,
50
+ "sort": sort_order.value,
51
+ "category": category.value if category else None,
52
+ "sub_category": sub_category.value if sub_category else None,
53
+ }
54
+
55
+ extras = [("location", loc.value) for loc in locations]
56
+
57
+ return _build_params(defaults, extras)
58
+
59
+
60
+ def build_car_params(
61
+ query: str | None = None,
62
+ *,
63
+ page: int = 1,
64
+ sort_order: CarSortOrder = CarSortOrder.RELEVANCE,
65
+ locations: list[Location] = [],
66
+ models: list[CarModel] = [],
67
+ price_from: int | None = None,
68
+ price_to: int | None = None,
69
+ year_from: int | None = None,
70
+ year_to: int | None = None,
71
+ milage_from: int | None = None,
72
+ milage_to: int | None = None,
73
+ colors: list[CarColor] = [],
74
+ transmissions: list[CarTransmission] = [],
75
+ horsepower_from: int | None = None,
76
+ horsepower_to: int | None = None,
77
+ wheel_drive: list[CarWheelDrive] = [],
78
+ org_id: int | None = None,
79
+ ) -> list[tuple[str, _ParamValue]]:
80
+ defaults: dict[str, _ParamValue] = {
81
+ "q": query,
82
+ "page": page,
83
+ "sort": sort_order.value,
84
+ "price_from": price_from,
85
+ "price_to": price_to,
86
+ "year_from": year_from,
87
+ "year_to": year_to,
88
+ "milage_from": milage_from,
89
+ "milage_to": milage_to,
90
+ "engine_effect_from": horsepower_from,
91
+ "engine_effect_to": horsepower_to,
92
+ "orgId": org_id,
93
+ }
94
+
95
+ extras: list[tuple[str, _ParamValue]] = []
96
+ extras.extend(("location", loc.value) for loc in locations)
97
+ extras.extend(("make", model.value) for model in models)
98
+ extras.extend(("exterior_colour", color.value) for color in colors)
99
+ extras.extend(("transmission", t.value) for t in transmissions)
100
+ extras.extend(("wheel_drive", w.value) for w in wheel_drive)
101
+
102
+ return _build_params(defaults, extras)
103
+
104
+
105
+ def build_boat_params(
106
+ query: str | None = None,
107
+ *,
108
+ page: int = 1,
109
+ sort_order: CarSortOrder = CarSortOrder.RELEVANCE,
110
+ types: list[BoatType] = [],
111
+ locations: list[Location] = [],
112
+ price_from: int | None = None,
113
+ price_to: int | None = None,
114
+ length_from: int | None = None,
115
+ length_to: int | None = None,
116
+ org_id: int | None = None,
117
+ ) -> list[tuple[str, _ParamValue]]:
118
+ defaults: dict[str, _ParamValue] = {
119
+ "q": query,
120
+ "page": page,
121
+ "sort": sort_order.value,
122
+ "price_from": price_from,
123
+ "price_to": price_to,
124
+ "length_feet_from": length_from,
125
+ "length_feet_to": length_to,
126
+ "orgId": org_id,
127
+ }
128
+
129
+ extras: list[tuple[str, _ParamValue]] = []
130
+ extras.extend(("class", t.value) for t in types)
131
+ extras.extend(("location", loc.value) for loc in locations)
132
+
133
+ return _build_params(defaults, extras)
134
+
135
+
136
+ def build_mc_params(
137
+ query: str | None = None,
138
+ *,
139
+ page: int = 1,
140
+ sort_order: McSortOrder = McSortOrder.RELEVANCE,
141
+ models: list[McModel] = [],
142
+ types: list[McType] = [],
143
+ locations: list[Location] = [],
144
+ price_from: int | None = None,
145
+ price_to: int | None = None,
146
+ engine_volume_from: int | None = None,
147
+ engine_volume_to: int | None = None,
148
+ org_id: int | None = None,
149
+ ) -> list[tuple[str, _ParamValue]]:
150
+ defaults: dict[str, _ParamValue] = {
151
+ "q": query,
152
+ "page": page,
153
+ "sort": sort_order.value,
154
+ "price_from": price_from,
155
+ "price_to": price_to,
156
+ "engine_volume_from": engine_volume_from,
157
+ "engine_volume_to": engine_volume_to,
158
+ "orgId": org_id,
159
+ }
160
+
161
+ extras: list[tuple[str, _ParamValue]] = []
162
+ extras.extend(("make", m.value) for m in models)
163
+ extras.extend(("location", loc.value) for loc in locations)
164
+ extras.extend(("type", t.value) for t in types)
165
+
166
+ return _build_params(defaults, extras)
@@ -100,7 +100,7 @@ class MobilityAd:
100
100
  return
101
101
 
102
102
  def _extract_specifications(self, grid: Tag, data: dict) -> None:
103
- specs_section = grid.find("section", class_="key-info-section")
103
+ specs_section = grid.find("section", class_=lambda x: x and "key-info" in x)
104
104
  if not specs_section:
105
105
  return
106
106
  dl = specs_section.find("dl")
@@ -108,9 +108,8 @@ class MobilityAd:
108
108
  return
109
109
 
110
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")
111
+ for dt in dl.find_all("dt"):
112
+ dd = dt.find_next_sibling("dd")
114
113
  if dt and dd:
115
114
  specifications[dt.get_text(strip=True)] = dd.get_text(strip=True)
116
115
 
@@ -193,7 +192,7 @@ class McAd(MobilityAd):
193
192
 
194
193
  def extend(self, data: dict, soup: BeautifulSoup, grid: Tag) -> None:
195
194
  if place_h2 := soup.find(
196
- "h2", text=lambda t: isinstance(t, NavigableString) and "Plats" in t
195
+ "h2", string=lambda t: isinstance(t, NavigableString) and "Plats" in t
197
196
  ):
198
197
  if link := place_h2.find_parent():
199
198
  data["location"] = link.get_text(strip=True)
@@ -0,0 +1,185 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import httpx
6
+
7
+ from ._params import (
8
+ build_boat_params,
9
+ build_car_params,
10
+ build_mc_params,
11
+ build_search_params,
12
+ )
13
+ from .ad_parser import BoatAd, CarAd, McAd, RecommerceAd
14
+ from .constants import (
15
+ BOAT_SEARCH_URL,
16
+ CAR_SEARCH_URL,
17
+ HEADERS,
18
+ MC_SEARCH_URL,
19
+ SEARCH_URL,
20
+ BoatType,
21
+ CarColor,
22
+ CarModel,
23
+ CarSortOrder,
24
+ CarTransmission,
25
+ CarWheelDrive,
26
+ Category,
27
+ Location,
28
+ McModel,
29
+ McSortOrder,
30
+ McType,
31
+ SortOrder,
32
+ SubCategory,
33
+ )
34
+
35
+
36
+ class AsyncBlocketAPI:
37
+ def __init__(self, client: httpx.AsyncClient | None = None) -> None:
38
+ self._client = client or httpx.AsyncClient()
39
+ self._owns_client = client is None
40
+
41
+ async def __aenter__(self) -> AsyncBlocketAPI:
42
+ return self
43
+
44
+ async def __aexit__(self, *_: object) -> None:
45
+ await self.aclose()
46
+
47
+ async def aclose(self) -> None:
48
+ await self._client.aclose()
49
+
50
+ async def search(
51
+ self,
52
+ query: str,
53
+ *,
54
+ page: int = 1,
55
+ sort_order: SortOrder = SortOrder.RELEVANCE,
56
+ locations: list[Location] = [],
57
+ category: Category | None = None,
58
+ sub_category: SubCategory | None = None,
59
+ ) -> dict[str, Any]:
60
+ params = build_search_params(
61
+ query,
62
+ page=page,
63
+ sort_order=sort_order,
64
+ locations=locations,
65
+ category=category,
66
+ sub_category=sub_category,
67
+ )
68
+ response = await self._client.get(SEARCH_URL, headers=HEADERS, params=params)
69
+ response.raise_for_status()
70
+ return response.json()
71
+
72
+ async def search_car(
73
+ self,
74
+ query: str | None = None,
75
+ *,
76
+ page: int = 1,
77
+ sort_order: CarSortOrder = CarSortOrder.RELEVANCE,
78
+ locations: list[Location] = [],
79
+ models: list[CarModel] = [],
80
+ price_from: int | None = None,
81
+ price_to: int | None = None,
82
+ year_from: int | None = None,
83
+ year_to: int | None = None,
84
+ milage_from: int | None = None,
85
+ milage_to: int | None = None,
86
+ colors: list[CarColor] = [],
87
+ transmissions: list[CarTransmission] = [],
88
+ horsepower_from: int | None = None,
89
+ horsepower_to: int | None = None,
90
+ wheel_drive: list[CarWheelDrive] = [],
91
+ org_id: int | None = None,
92
+ ) -> dict[str, Any]:
93
+ params = build_car_params(
94
+ query,
95
+ page=page,
96
+ sort_order=sort_order,
97
+ locations=locations,
98
+ models=models,
99
+ price_from=price_from,
100
+ price_to=price_to,
101
+ year_from=year_from,
102
+ year_to=year_to,
103
+ milage_from=milage_from,
104
+ milage_to=milage_to,
105
+ colors=colors,
106
+ transmissions=transmissions,
107
+ horsepower_from=horsepower_from,
108
+ horsepower_to=horsepower_to,
109
+ wheel_drive=wheel_drive,
110
+ org_id=org_id,
111
+ )
112
+ response = await self._client.get(
113
+ CAR_SEARCH_URL, headers=HEADERS, params=params
114
+ )
115
+ response.raise_for_status()
116
+ return response.json()
117
+
118
+ async def search_boat(
119
+ self,
120
+ query: str | None = None,
121
+ *,
122
+ page: int = 1,
123
+ sort_order: CarSortOrder = CarSortOrder.RELEVANCE,
124
+ types: list[BoatType] = [],
125
+ locations: list[Location] = [],
126
+ price_from: int | None = None,
127
+ price_to: int | None = None,
128
+ length_from: int | None = None,
129
+ length_to: int | None = None,
130
+ org_id: int | None = None,
131
+ ) -> Any:
132
+ params = build_boat_params(
133
+ query,
134
+ page=page,
135
+ sort_order=sort_order,
136
+ types=types,
137
+ locations=locations,
138
+ price_from=price_from,
139
+ price_to=price_to,
140
+ length_from=length_from,
141
+ length_to=length_to,
142
+ org_id=org_id,
143
+ )
144
+ response = await self._client.get(
145
+ BOAT_SEARCH_URL, headers=HEADERS, params=params
146
+ )
147
+ response.raise_for_status()
148
+ return response.json()
149
+
150
+ async def search_mc(
151
+ self,
152
+ query: str | None = None,
153
+ *,
154
+ page: int = 1,
155
+ sort_order: McSortOrder = McSortOrder.RELEVANCE,
156
+ models: list[McModel] = [],
157
+ types: list[McType] = [],
158
+ locations: list[Location] = [],
159
+ price_from: int | None = None,
160
+ price_to: int | None = None,
161
+ engine_volume_from: int | None = None,
162
+ engine_volume_to: int | None = None,
163
+ org_id: int | None = None,
164
+ ) -> dict[str, Any]:
165
+ params = build_mc_params(
166
+ query,
167
+ page=page,
168
+ sort_order=sort_order,
169
+ models=models,
170
+ types=types,
171
+ locations=locations,
172
+ price_from=price_from,
173
+ price_to=price_to,
174
+ engine_volume_from=engine_volume_from,
175
+ engine_volume_to=engine_volume_to,
176
+ org_id=org_id,
177
+ )
178
+ response = await self._client.get(MC_SEARCH_URL, headers=HEADERS, params=params)
179
+ response.raise_for_status()
180
+ return response.json()
181
+
182
+ async def get_ad(self, ad: RecommerceAd | CarAd | BoatAd | McAd) -> dict[str, Any]:
183
+ response = await self._client.get(ad.url, headers=HEADERS)
184
+ response.raise_for_status()
185
+ return ad.parse(response)
@@ -0,0 +1,166 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import httpx
6
+
7
+ from ._params import (
8
+ _ParamValue,
9
+ build_boat_params,
10
+ build_car_params,
11
+ build_mc_params,
12
+ build_search_params,
13
+ )
14
+ from .ad_parser import BoatAd, CarAd, McAd, RecommerceAd
15
+ from .constants import (
16
+ BOAT_SEARCH_URL,
17
+ CAR_SEARCH_URL,
18
+ HEADERS,
19
+ MC_SEARCH_URL,
20
+ SEARCH_URL,
21
+ BoatType,
22
+ CarColor,
23
+ CarModel,
24
+ CarSortOrder,
25
+ CarTransmission,
26
+ CarWheelDrive,
27
+ Category,
28
+ Location,
29
+ McModel,
30
+ McSortOrder,
31
+ McType,
32
+ SortOrder,
33
+ SubCategory,
34
+ )
35
+
36
+
37
+ def _request(*, url: str, params: list[tuple[str, _ParamValue]]) -> httpx.Response:
38
+ response = httpx.get(url, headers=HEADERS, params=params)
39
+ response.raise_for_status()
40
+ return response
41
+
42
+
43
+ class BlocketAPI:
44
+ def search(
45
+ self,
46
+ query: str,
47
+ *,
48
+ page: int = 1,
49
+ sort_order: SortOrder = SortOrder.RELEVANCE,
50
+ locations: list[Location] = [],
51
+ category: Category | None = None,
52
+ sub_category: SubCategory | None = None,
53
+ ) -> dict[str, Any]:
54
+ params = build_search_params(
55
+ query,
56
+ page=page,
57
+ sort_order=sort_order,
58
+ locations=locations,
59
+ category=category,
60
+ sub_category=sub_category,
61
+ )
62
+ return _request(url=SEARCH_URL, params=params).json()
63
+
64
+ def search_car(
65
+ self,
66
+ query: str | None = None,
67
+ *,
68
+ page: int = 1,
69
+ sort_order: CarSortOrder = CarSortOrder.RELEVANCE,
70
+ locations: list[Location] = [],
71
+ models: list[CarModel] = [],
72
+ price_from: int | None = None,
73
+ price_to: int | None = None,
74
+ year_from: int | None = None,
75
+ year_to: int | None = None,
76
+ milage_from: int | None = None,
77
+ milage_to: int | None = None,
78
+ colors: list[CarColor] = [],
79
+ transmissions: list[CarTransmission] = [],
80
+ horsepower_from: int | None = None,
81
+ horsepower_to: int | None = None,
82
+ wheel_drive: list[CarWheelDrive] = [],
83
+ org_id: int | None = None,
84
+ ) -> dict[str, Any]:
85
+ params = build_car_params(
86
+ query,
87
+ page=page,
88
+ sort_order=sort_order,
89
+ locations=locations,
90
+ models=models,
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
+ colors=colors,
98
+ transmissions=transmissions,
99
+ horsepower_from=horsepower_from,
100
+ horsepower_to=horsepower_to,
101
+ wheel_drive=wheel_drive,
102
+ org_id=org_id,
103
+ )
104
+ return _request(url=CAR_SEARCH_URL, params=params).json()
105
+
106
+ def search_boat(
107
+ self,
108
+ query: str | None = None,
109
+ *,
110
+ page: int = 1,
111
+ sort_order: CarSortOrder = CarSortOrder.RELEVANCE,
112
+ types: list[BoatType] = [],
113
+ locations: list[Location] = [],
114
+ price_from: int | None = None,
115
+ price_to: int | None = None,
116
+ length_from: int | None = None,
117
+ length_to: int | None = None,
118
+ org_id: int | None = None,
119
+ ) -> Any:
120
+ params = build_boat_params(
121
+ query,
122
+ page=page,
123
+ sort_order=sort_order,
124
+ types=types,
125
+ locations=locations,
126
+ price_from=price_from,
127
+ price_to=price_to,
128
+ length_from=length_from,
129
+ length_to=length_to,
130
+ org_id=org_id,
131
+ )
132
+ return _request(url=BOAT_SEARCH_URL, params=params).json()
133
+
134
+ def search_mc(
135
+ self,
136
+ query: str | None = None,
137
+ *,
138
+ page: int = 1,
139
+ sort_order: McSortOrder = McSortOrder.RELEVANCE,
140
+ models: list[McModel] = [],
141
+ types: list[McType] = [],
142
+ locations: list[Location] = [],
143
+ price_from: int | None = None,
144
+ price_to: int | None = None,
145
+ engine_volume_from: int | None = None,
146
+ engine_volume_to: int | None = None,
147
+ org_id: int | None = None,
148
+ ) -> dict[str, Any]:
149
+ params = build_mc_params(
150
+ query,
151
+ page=page,
152
+ sort_order=sort_order,
153
+ models=models,
154
+ types=types,
155
+ locations=locations,
156
+ price_from=price_from,
157
+ price_to=price_to,
158
+ engine_volume_from=engine_volume_from,
159
+ engine_volume_to=engine_volume_to,
160
+ org_id=org_id,
161
+ )
162
+ return _request(url=MC_SEARCH_URL, params=params).json()
163
+
164
+ def get_ad(self, ad: RecommerceAd | CarAd | BoatAd | McAd) -> dict[str, Any]:
165
+ response = _request(url=ad.url, params=[])
166
+ return ad.parse(response)
@@ -1,6 +1,11 @@
1
1
  from enum import IntEnum, StrEnum
2
2
 
3
3
  SITE_URL = "https://www.blocket.se"
4
+ SEARCH_URL = f"{SITE_URL}/recommerce/forsale/search/api/search/SEARCH_ID_BAP_COMMON"
5
+ CAR_SEARCH_URL = f"{SITE_URL}/mobility/search/api/search/SEARCH_ID_CAR_USED"
6
+ BOAT_SEARCH_URL = f"{SITE_URL}/mobility/search/api/search/SEARCH_ID_BOAT_USED"
7
+ MC_SEARCH_URL = f"{SITE_URL}/mobility/search/api/search/SEARCH_ID_MC_USED"
8
+
4
9
  HEADERS = {
5
10
  "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0"
6
11
  }
@@ -1,44 +1,45 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: blocket_api
3
- Version: 0.5.1
3
+ Version: 0.5.3
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
7
7
  Description-Content-Type: text/markdown
8
8
  License-File: LICENSE
9
9
  Requires-Dist: httpx>=0.28.1
10
- Requires-Dist: pre-commit>=4.5.1
11
- Requires-Dist: pytest>=9.0.2
12
- Requires-Dist: respx>=0.22.0
13
- Requires-Dist: mypy>=1.19.1
14
- Requires-Dist: mkdocs>=1.6.1
15
- Requires-Dist: mkdocs-material>=9.7.1
16
- Requires-Dist: mkdocs-swagger-ui-tag>=0.7.2
17
- Requires-Dist: beautifulsoup4>=4.14.3
18
- Requires-Dist: ruff>=0.15.12
10
+ Requires-Dist: beautifulsoup4>=4.15.0
11
+ Provides-Extra: dev
12
+ Requires-Dist: pre-commit>=4.6.2; extra == "dev"
13
+ Requires-Dist: pytest>=9.1.1; extra == "dev"
14
+ Requires-Dist: pytest-asyncio>=1.4.0; extra == "dev"
15
+ Requires-Dist: respx>=0.23.1; extra == "dev"
16
+ Requires-Dist: mypy>=2.3.1; extra == "dev"
17
+ Requires-Dist: ruff>=0.16.6; extra == "dev"
18
+ Requires-Dist: flask>=3.1.3; extra == "dev"
19
+ Requires-Dist: htpy>=26.5.1; extra == "dev"
20
+ Requires-Dist: gunicorn>=26.2.0; extra == "dev"
19
21
  Dynamic: license-file
20
22
 
21
- <p align="center">
22
- <img src="https://blocket-api.se/blocket-api.png" />
23
- </p>
24
-
25
23
  # BlocketAPI
26
24
 
27
- [![PyPI version](https://img.shields.io/pypi/v/blocket_api?style=for-the-badge)](https://pypi.org/project/blocket_api/) [![License](https://img.shields.io/badge/license-WTFPL-green?style=for-the-badge)](https://github.com/dunderrrrrr/blocket_api/blob/main/LICENSE) ![Python](https://img.shields.io/badge/Python-3.10-blue?style=for-the-badge) ![PyPI - Downloads](https://img.shields.io/pypi/dm/blocket_api?style=for-the-badge&color=%23dbce58)
25
+ [![PyPI version](https://img.shields.io/pypi/v/blocket_api)](https://pypi.org/project/blocket_api/) [![License](https://img.shields.io/badge/license-WTFPL-green)](https://github.com/dunderrrrrr/blocket_api/blob/main/LICENSE) ![Python](https://img.shields.io/badge/Python-3.10-blue) ![PyPI - Downloads](https://static.pepy.tech/personalized-badge/blocket-api?period=monthly&units=INTERNATIONAL_SYSTEM&left_color=GREY&right_color=RED&left_text=downloads%2Fmonth)
28
26
 
29
27
  BlocketAPI allows users to search [blocket.se](https://blocket.se/) for ads.
30
28
 
31
- > 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.
29
+ > 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.
32
30
 
33
31
  ## 🧑‍💻️ Install
34
32
 
35
33
  Install BlocketAPI via PyPI...
36
34
 
37
35
  ```sh
38
- pip install blocket-api
36
+ pip install blocket-api # using pip
37
+ uv add blocket-api # using uv
39
38
  ```
40
39
 
41
- or use [blocket-api.se](https://blocket-api.se) without installing anything!
40
+ ## 📜 Docs
41
+
42
+ The full documentation can be found at [blocket-api.se](https://blocket-api.se/).
42
43
 
43
44
  ## 💁‍♀️ Usage
44
45
 
@@ -119,6 +120,19 @@ api.get_ad(BoatAd(12345678))
119
120
  api.get_ad(McAd(12345678))
120
121
  ```
121
122
 
123
+ ### Async support
124
+
125
+ ```py
126
+ import asyncio
127
+ from blocket_api import AsyncBlocketAPI
128
+
129
+ async def main():
130
+ async with AsyncBlocketAPI() as api:
131
+ results = await api.search("iPhone 15")
132
+
133
+ asyncio.run(main())
134
+ ```
135
+
122
136
  ## 📝 Notes
123
137
 
124
138
  - REST API: https://blocket-api.se
@@ -2,7 +2,9 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  blocket_api/__init__.py
5
+ blocket_api/_params.py
5
6
  blocket_api/ad_parser.py
7
+ blocket_api/async_blocket.py
6
8
  blocket_api/blocket.py
7
9
  blocket_api/constants.py
8
10
  blocket_api.egg-info/PKG-INFO
@@ -0,0 +1,13 @@
1
+ httpx>=0.28.1
2
+ beautifulsoup4>=4.15.0
3
+
4
+ [dev]
5
+ pre-commit>=4.6.2
6
+ pytest>=9.1.1
7
+ pytest-asyncio>=1.4.0
8
+ respx>=0.23.1
9
+ mypy>=2.3.1
10
+ ruff>=0.16.6
11
+ flask>=3.1.3
12
+ htpy>=26.5.1
13
+ gunicorn>=26.2.0
@@ -0,0 +1,34 @@
1
+ [project]
2
+ name = "blocket_api"
3
+ version = "0.5.3"
4
+ description = "A python API wrapper for blocket.se"
5
+ authors = [{name = "dunderrrrrr", email = "emil.bjorkroth@gmail.com"}]
6
+ readme = "README.md"
7
+ requires-python = ">=3.10"
8
+ dependencies = [
9
+ "httpx>=0.28.1",
10
+ "beautifulsoup4>=4.15.0",
11
+ ]
12
+
13
+ [project.optional-dependencies]
14
+ dev = [
15
+ "pre-commit>=4.6.2",
16
+ "pytest>=9.1.1",
17
+ "pytest-asyncio>=1.4.0",
18
+ "respx>=0.23.1",
19
+ "mypy>=2.3.1",
20
+ "ruff>=0.16.6",
21
+ "flask>=3.1.3",
22
+ "htpy>=26.5.1",
23
+ "gunicorn>=26.2.0",
24
+ ]
25
+
26
+
27
+ [tool.pytest.ini_options]
28
+ testpaths = [
29
+ "tests/*",
30
+ ]
31
+ asyncio_mode = "auto"
32
+
33
+ [tool.ruff.lint]
34
+ ignore = ["B006", "RUF012", "SIM102", "PYI034"]
@@ -1,196 +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 BoatAd, CarAd, McAd, RecommerceAd
10
- from .constants import (
11
- HEADERS,
12
- SITE_URL,
13
- BoatType,
14
- CarColor,
15
- CarModel,
16
- CarSortOrder,
17
- CarTransmission,
18
- CarWheelDrive,
19
- Category,
20
- Location,
21
- McModel,
22
- McSortOrder,
23
- McType,
24
- SortOrder,
25
- SubCategory,
26
- )
27
-
28
-
29
- @dataclass(frozen=True)
30
- class QueryParam:
31
- name: str
32
- value: str | int
33
-
34
-
35
- def _request(*, url: str, params: list[QueryParam]) -> Response:
36
- response = httpx.get(
37
- url,
38
- headers=HEADERS,
39
- params=[(param.name, param.value) for param in params],
40
- )
41
- response.raise_for_status()
42
- return response
43
-
44
-
45
- @dataclass(frozen=True)
46
- class BlocketAPI:
47
- def search(
48
- self,
49
- query: str,
50
- *,
51
- page: int = 1,
52
- sort_order: SortOrder = SortOrder.RELEVANCE,
53
- locations: list[Location] = [],
54
- category: Category | None = None,
55
- sub_category: SubCategory | None = None,
56
- ) -> dict[str, Any]:
57
- if category and sub_category:
58
- raise AssertionError("Cannot specify both category and sub_categories")
59
-
60
- url = f"{SITE_URL}/recommerce/forsale/search/api/search/SEARCH_ID_BAP_COMMON"
61
-
62
- param_dict: dict[str, str | int | None] = {
63
- "q": query,
64
- "page": page,
65
- "sort": sort_order.value,
66
- "category": category.value if category else None,
67
- "sub_category": sub_category.value if sub_category else None,
68
- }
69
-
70
- params = [QueryParam(k, v) for k, v in param_dict.items() if v is not None]
71
-
72
- params.extend(QueryParam("location", loc.value) for loc in locations)
73
-
74
- return _request(url=url, params=params).json()
75
-
76
- def search_car(
77
- self,
78
- query: str | None = None,
79
- *,
80
- page: int = 1,
81
- sort_order: CarSortOrder = CarSortOrder.RELEVANCE,
82
- locations: list[Location] = [],
83
- models: list[CarModel] = [],
84
- price_from: int | None = None,
85
- price_to: int | None = None,
86
- year_from: int | None = None,
87
- year_to: int | None = None,
88
- milage_from: int | None = None,
89
- milage_to: int | None = None,
90
- colors: list[CarColor] = [],
91
- transmissions: list[CarTransmission] = [],
92
- horsepower_from: int | None = None,
93
- horsepower_to: int | None = None,
94
- wheel_drive: list[CarWheelDrive] = [],
95
- org_id: int | None = None,
96
- ) -> dict[str, Any]:
97
- url = f"{SITE_URL}/mobility/search/api/search/SEARCH_ID_CAR_USED"
98
-
99
- param_dict: dict[str, str | int | None] = {
100
- "q": query,
101
- "page": page,
102
- "sort": sort_order.value,
103
- "price_from": price_from,
104
- "price_to": price_to,
105
- "year_from": year_from,
106
- "year_to": year_to,
107
- "milage_from": milage_from,
108
- "milage_to": milage_to,
109
- "engine_effect_from": horsepower_from,
110
- "engine_effect_to": horsepower_to,
111
- "orgId": org_id,
112
- }
113
-
114
- params = [QueryParam(k, v) for k, v in param_dict.items() if v is not None]
115
-
116
- params.extend(QueryParam("location", loc.value) for loc in locations)
117
- params.extend(QueryParam("make", model.value) for model in models)
118
- params.extend(QueryParam("exterior_colour", color.value) for color in colors)
119
- params.extend(QueryParam("transmission", t.value) for t in transmissions)
120
- params.extend(QueryParam("wheel_drive", w.value) for w in wheel_drive)
121
-
122
- return _request(url=url, params=params).json()
123
-
124
- def search_boat(
125
- self,
126
- query: str | None = None,
127
- *,
128
- page: int = 1,
129
- sort_order: CarSortOrder = CarSortOrder.RELEVANCE,
130
- types: list[BoatType] = [],
131
- locations: list[Location] = [],
132
- price_from: int | None = None,
133
- price_to: int | None = None,
134
- length_from: int | None = None,
135
- length_to: int | None = None,
136
- org_id: int | None = None,
137
- ) -> Any:
138
- url = f"{SITE_URL}/mobility/search/api/search/SEARCH_ID_BOAT_USED"
139
-
140
- param_dict: dict[str, str | int | None] = {
141
- "q": query,
142
- "page": page,
143
- "sort": sort_order.value,
144
- "price_from": price_from,
145
- "price_to": price_to,
146
- "length_feet_from": length_from,
147
- "length_feet_to": length_to,
148
- "orgId": org_id,
149
- }
150
-
151
- params = [QueryParam(k, v) for k, v in param_dict.items() if v is not None]
152
-
153
- params.extend(QueryParam("class", t.value) for t in types)
154
- params.extend(QueryParam("location", loc.value) for loc in locations)
155
-
156
- return _request(url=url, params=params).json()
157
-
158
- def search_mc(
159
- self,
160
- query: str | None = None,
161
- *,
162
- page: int = 1,
163
- sort_order: McSortOrder = McSortOrder.RELEVANCE,
164
- models: list[McModel] = [],
165
- types: list[McType] = [],
166
- locations: list[Location] = [],
167
- price_from: int | None = None,
168
- price_to: int | None = None,
169
- engine_volume_from: int | None = None,
170
- engine_volume_to: int | None = None,
171
- org_id: int | None = None,
172
- ) -> dict[str, Any]:
173
- url = f"{SITE_URL}/mobility/search/api/search/SEARCH_ID_MC_USED"
174
-
175
- param_dict: dict[str, str | int | None] = {
176
- "q": query,
177
- "page": page,
178
- "sort": sort_order.value,
179
- "price_from": price_from,
180
- "price_to": price_to,
181
- "engine_volume_from": engine_volume_from,
182
- "engine_volume_to": engine_volume_to,
183
- "orgId": org_id,
184
- }
185
-
186
- params = [QueryParam(k, v) for k, v in param_dict.items() if v is not None]
187
-
188
- params.extend(QueryParam("make", m.value) for m in models)
189
- params.extend(QueryParam("location", loc.value) for loc in locations)
190
- params.extend(QueryParam("type", t.value) for t in types)
191
-
192
- return _request(url=url, params=params).json()
193
-
194
- def get_ad(self, ad: RecommerceAd | CarAd | BoatAd | McAd) -> dict[str, Any]:
195
- response = _request(url=ad.url, params=[])
196
- return ad.parse(response)
@@ -1,10 +0,0 @@
1
- httpx>=0.28.1
2
- pre-commit>=4.5.1
3
- pytest>=9.0.2
4
- respx>=0.22.0
5
- mypy>=1.19.1
6
- mkdocs>=1.6.1
7
- mkdocs-material>=9.7.1
8
- mkdocs-swagger-ui-tag>=0.7.2
9
- beautifulsoup4>=4.14.3
10
- ruff>=0.15.12
@@ -1,25 +0,0 @@
1
- [project]
2
- name = "blocket_api"
3
- version = "0.5.1"
4
- description = "A python API wrapper for blocket.se"
5
- authors = [{name = "dunderrrrrr", email = "emil.bjorkroth@gmail.com"}]
6
- readme = "README.md"
7
- requires-python = ">=3.10"
8
- dependencies = [
9
- "httpx>=0.28.1",
10
- "pre-commit>=4.5.1",
11
- "pytest>=9.0.2",
12
- "respx>=0.22.0",
13
- "mypy>=1.19.1",
14
- "mkdocs>=1.6.1",
15
- "mkdocs-material>=9.7.1",
16
- "mkdocs-swagger-ui-tag>=0.7.2",
17
- "beautifulsoup4>=4.14.3",
18
- "ruff>=0.15.12",
19
- ]
20
-
21
-
22
- [tool.pytest.ini_options]
23
- testpaths = [
24
- "tests/*",
25
- ]
File without changes
File without changes