blocket-api 0.5.1__tar.gz → 0.5.2__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,30 +1,28 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: blocket_api
3
- Version: 0.5.1
3
+ Version: 0.5.2
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
10
  Requires-Dist: beautifulsoup4>=4.14.3
18
- Requires-Dist: ruff>=0.15.12
11
+ Provides-Extra: dev
12
+ Requires-Dist: pre-commit>=4.5.1; extra == "dev"
13
+ Requires-Dist: pytest>=9.0.2; extra == "dev"
14
+ Requires-Dist: pytest-asyncio>=0.26.0; extra == "dev"
15
+ Requires-Dist: respx>=0.22.0; extra == "dev"
16
+ Requires-Dist: mypy>=1.19.1; extra == "dev"
17
+ Requires-Dist: ruff>=0.9.0; 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.0.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
 
@@ -35,10 +33,13 @@ BlocketAPI allows users to search [blocket.se](https://blocket.se/) for ads.
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,10 +1,6 @@
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
 
@@ -15,10 +11,13 @@ BlocketAPI allows users to search [blocket.se](https://blocket.se/) for ads.
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,6 +18,7 @@ from .constants import (
17
18
  )
18
19
 
19
20
  __all__ = [
21
+ "AsyncBlocketAPI",
20
22
  "BlocketAPI",
21
23
  "Location",
22
24
  "BoatAd",
@@ -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)
@@ -0,0 +1,187 @@
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
+ )
21
+ from .constants import (
22
+ BoatType,
23
+ CarColor,
24
+ CarModel,
25
+ CarSortOrder,
26
+ CarTransmission,
27
+ CarWheelDrive,
28
+ Category,
29
+ Location,
30
+ McModel,
31
+ McSortOrder,
32
+ McType,
33
+ SortOrder,
34
+ SubCategory,
35
+ )
36
+
37
+
38
+ class AsyncBlocketAPI:
39
+ def __init__(self, client: httpx.AsyncClient | None = None) -> None:
40
+ self._client = client or httpx.AsyncClient()
41
+ self._owns_client = client is None
42
+
43
+ async def __aenter__(self) -> AsyncBlocketAPI:
44
+ return self
45
+
46
+ async def __aexit__(self, *_: Any) -> None:
47
+ await self.aclose()
48
+
49
+ async def aclose(self) -> None:
50
+ await self._client.aclose()
51
+
52
+ async def search(
53
+ self,
54
+ query: str,
55
+ *,
56
+ page: int = 1,
57
+ sort_order: SortOrder = SortOrder.RELEVANCE,
58
+ locations: list[Location] = [],
59
+ category: Category | None = None,
60
+ sub_category: SubCategory | None = None,
61
+ ) -> dict[str, Any]:
62
+ params = build_search_params(
63
+ query,
64
+ page=page,
65
+ sort_order=sort_order,
66
+ locations=locations,
67
+ category=category,
68
+ sub_category=sub_category,
69
+ )
70
+ response = await self._client.get(SEARCH_URL, headers=HEADERS, params=params)
71
+ response.raise_for_status()
72
+ return response.json()
73
+
74
+ async def search_car(
75
+ self,
76
+ query: str | None = None,
77
+ *,
78
+ page: int = 1,
79
+ sort_order: CarSortOrder = CarSortOrder.RELEVANCE,
80
+ locations: list[Location] = [],
81
+ models: list[CarModel] = [],
82
+ price_from: int | None = None,
83
+ price_to: int | None = None,
84
+ year_from: int | None = None,
85
+ year_to: int | None = None,
86
+ milage_from: int | None = None,
87
+ milage_to: int | None = None,
88
+ colors: list[CarColor] = [],
89
+ transmissions: list[CarTransmission] = [],
90
+ horsepower_from: int | None = None,
91
+ horsepower_to: int | None = None,
92
+ wheel_drive: list[CarWheelDrive] = [],
93
+ org_id: int | None = None,
94
+ ) -> dict[str, Any]:
95
+ params = build_car_params(
96
+ query,
97
+ page=page,
98
+ sort_order=sort_order,
99
+ locations=locations,
100
+ models=models,
101
+ price_from=price_from,
102
+ price_to=price_to,
103
+ year_from=year_from,
104
+ year_to=year_to,
105
+ milage_from=milage_from,
106
+ milage_to=milage_to,
107
+ colors=colors,
108
+ transmissions=transmissions,
109
+ horsepower_from=horsepower_from,
110
+ horsepower_to=horsepower_to,
111
+ wheel_drive=wheel_drive,
112
+ org_id=org_id,
113
+ )
114
+ response = await self._client.get(
115
+ CAR_SEARCH_URL, headers=HEADERS, params=params
116
+ )
117
+ response.raise_for_status()
118
+ return response.json()
119
+
120
+ async def search_boat(
121
+ self,
122
+ query: str | None = None,
123
+ *,
124
+ page: int = 1,
125
+ sort_order: CarSortOrder = CarSortOrder.RELEVANCE,
126
+ types: list[BoatType] = [],
127
+ locations: list[Location] = [],
128
+ price_from: int | None = None,
129
+ price_to: int | None = None,
130
+ length_from: int | None = None,
131
+ length_to: int | None = None,
132
+ org_id: int | None = None,
133
+ ) -> Any:
134
+ params = build_boat_params(
135
+ query,
136
+ page=page,
137
+ sort_order=sort_order,
138
+ types=types,
139
+ locations=locations,
140
+ price_from=price_from,
141
+ price_to=price_to,
142
+ length_from=length_from,
143
+ length_to=length_to,
144
+ org_id=org_id,
145
+ )
146
+ response = await self._client.get(
147
+ BOAT_SEARCH_URL, headers=HEADERS, params=params
148
+ )
149
+ response.raise_for_status()
150
+ return response.json()
151
+
152
+ async def search_mc(
153
+ self,
154
+ query: str | None = None,
155
+ *,
156
+ page: int = 1,
157
+ sort_order: McSortOrder = McSortOrder.RELEVANCE,
158
+ models: list[McModel] = [],
159
+ types: list[McType] = [],
160
+ locations: list[Location] = [],
161
+ price_from: int | None = None,
162
+ price_to: int | None = None,
163
+ engine_volume_from: int | None = None,
164
+ engine_volume_to: int | None = None,
165
+ org_id: int | None = None,
166
+ ) -> dict[str, Any]:
167
+ params = build_mc_params(
168
+ query,
169
+ page=page,
170
+ sort_order=sort_order,
171
+ models=models,
172
+ types=types,
173
+ locations=locations,
174
+ price_from=price_from,
175
+ price_to=price_to,
176
+ engine_volume_from=engine_volume_from,
177
+ engine_volume_to=engine_volume_to,
178
+ org_id=org_id,
179
+ )
180
+ response = await self._client.get(MC_SEARCH_URL, headers=HEADERS, params=params)
181
+ response.raise_for_status()
182
+ return response.json()
183
+
184
+ async def get_ad(self, ad: RecommerceAd | CarAd | BoatAd | McAd) -> dict[str, Any]:
185
+ response = await self._client.get(ad.url, headers=HEADERS)
186
+ response.raise_for_status()
187
+ return ad.parse(response)
@@ -0,0 +1,168 @@
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
+ )
22
+ from .constants import (
23
+ BoatType,
24
+ CarColor,
25
+ CarModel,
26
+ CarSortOrder,
27
+ CarTransmission,
28
+ CarWheelDrive,
29
+ Category,
30
+ Location,
31
+ McModel,
32
+ McSortOrder,
33
+ McType,
34
+ SortOrder,
35
+ SubCategory,
36
+ )
37
+
38
+
39
+ def _request(*, url: str, params: list[tuple[str, _ParamValue]]) -> httpx.Response:
40
+ response = httpx.get(url, headers=HEADERS, params=params)
41
+ response.raise_for_status()
42
+ return response
43
+
44
+
45
+ class BlocketAPI:
46
+ def search(
47
+ self,
48
+ query: str,
49
+ *,
50
+ page: int = 1,
51
+ sort_order: SortOrder = SortOrder.RELEVANCE,
52
+ locations: list[Location] = [],
53
+ category: Category | None = None,
54
+ sub_category: SubCategory | None = None,
55
+ ) -> dict[str, Any]:
56
+ params = build_search_params(
57
+ query,
58
+ page=page,
59
+ sort_order=sort_order,
60
+ locations=locations,
61
+ category=category,
62
+ sub_category=sub_category,
63
+ )
64
+ return _request(url=SEARCH_URL, params=params).json()
65
+
66
+ def search_car(
67
+ self,
68
+ query: str | None = None,
69
+ *,
70
+ page: int = 1,
71
+ sort_order: CarSortOrder = CarSortOrder.RELEVANCE,
72
+ locations: list[Location] = [],
73
+ models: list[CarModel] = [],
74
+ price_from: int | None = None,
75
+ price_to: int | None = None,
76
+ year_from: int | None = None,
77
+ year_to: int | None = None,
78
+ milage_from: int | None = None,
79
+ milage_to: int | None = None,
80
+ colors: list[CarColor] = [],
81
+ transmissions: list[CarTransmission] = [],
82
+ horsepower_from: int | None = None,
83
+ horsepower_to: int | None = None,
84
+ wheel_drive: list[CarWheelDrive] = [],
85
+ org_id: int | None = None,
86
+ ) -> dict[str, Any]:
87
+ params = build_car_params(
88
+ query,
89
+ page=page,
90
+ sort_order=sort_order,
91
+ locations=locations,
92
+ models=models,
93
+ price_from=price_from,
94
+ price_to=price_to,
95
+ year_from=year_from,
96
+ year_to=year_to,
97
+ milage_from=milage_from,
98
+ milage_to=milage_to,
99
+ colors=colors,
100
+ transmissions=transmissions,
101
+ horsepower_from=horsepower_from,
102
+ horsepower_to=horsepower_to,
103
+ wheel_drive=wheel_drive,
104
+ org_id=org_id,
105
+ )
106
+ return _request(url=CAR_SEARCH_URL, params=params).json()
107
+
108
+ def search_boat(
109
+ self,
110
+ query: str | None = None,
111
+ *,
112
+ page: int = 1,
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
+ org_id: int | None = None,
121
+ ) -> Any:
122
+ params = build_boat_params(
123
+ query,
124
+ page=page,
125
+ sort_order=sort_order,
126
+ types=types,
127
+ locations=locations,
128
+ price_from=price_from,
129
+ price_to=price_to,
130
+ length_from=length_from,
131
+ length_to=length_to,
132
+ org_id=org_id,
133
+ )
134
+ return _request(url=BOAT_SEARCH_URL, params=params).json()
135
+
136
+ def search_mc(
137
+ self,
138
+ query: str | None = None,
139
+ *,
140
+ page: int = 1,
141
+ sort_order: McSortOrder = McSortOrder.RELEVANCE,
142
+ models: list[McModel] = [],
143
+ types: list[McType] = [],
144
+ locations: list[Location] = [],
145
+ price_from: int | None = None,
146
+ price_to: int | None = None,
147
+ engine_volume_from: int | None = None,
148
+ engine_volume_to: int | None = None,
149
+ org_id: int | None = None,
150
+ ) -> dict[str, Any]:
151
+ params = build_mc_params(
152
+ query,
153
+ page=page,
154
+ sort_order=sort_order,
155
+ models=models,
156
+ types=types,
157
+ locations=locations,
158
+ price_from=price_from,
159
+ price_to=price_to,
160
+ engine_volume_from=engine_volume_from,
161
+ engine_volume_to=engine_volume_to,
162
+ org_id=org_id,
163
+ )
164
+ return _request(url=MC_SEARCH_URL, params=params).json()
165
+
166
+ def get_ad(self, ad: RecommerceAd | CarAd | BoatAd | McAd) -> dict[str, Any]:
167
+ response = _request(url=ad.url, params=[])
168
+ 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,30 +1,28 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: blocket_api
3
- Version: 0.5.1
3
+ Version: 0.5.2
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
10
  Requires-Dist: beautifulsoup4>=4.14.3
18
- Requires-Dist: ruff>=0.15.12
11
+ Provides-Extra: dev
12
+ Requires-Dist: pre-commit>=4.5.1; extra == "dev"
13
+ Requires-Dist: pytest>=9.0.2; extra == "dev"
14
+ Requires-Dist: pytest-asyncio>=0.26.0; extra == "dev"
15
+ Requires-Dist: respx>=0.22.0; extra == "dev"
16
+ Requires-Dist: mypy>=1.19.1; extra == "dev"
17
+ Requires-Dist: ruff>=0.9.0; 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.0.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
 
@@ -35,10 +33,13 @@ BlocketAPI allows users to search [blocket.se](https://blocket.se/) for ads.
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
@@ -1,10 +1,13 @@
1
1
  httpx>=0.28.1
2
+ beautifulsoup4>=4.14.3
3
+
4
+ [dev]
2
5
  pre-commit>=4.5.1
3
6
  pytest>=9.0.2
7
+ pytest-asyncio>=0.26.0
4
8
  respx>=0.22.0
5
9
  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
10
+ ruff>=0.9.0
11
+ flask>=3.1.3
12
+ htpy>=26.5.1
13
+ gunicorn>=26.0.0
@@ -1,21 +1,26 @@
1
1
  [project]
2
2
  name = "blocket_api"
3
- version = "0.5.1"
3
+ version = "0.5.2"
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"
7
7
  requires-python = ">=3.10"
8
8
  dependencies = [
9
9
  "httpx>=0.28.1",
10
+ "beautifulsoup4>=4.14.3",
11
+ ]
12
+
13
+ [project.optional-dependencies]
14
+ dev = [
10
15
  "pre-commit>=4.5.1",
11
16
  "pytest>=9.0.2",
17
+ "pytest-asyncio>=0.26.0",
12
18
  "respx>=0.22.0",
13
19
  "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",
20
+ "ruff>=0.9.0",
21
+ "flask>=3.1.3",
22
+ "htpy>=26.5.1",
23
+ "gunicorn>=26.0.0",
19
24
  ]
20
25
 
21
26
 
@@ -23,3 +28,4 @@ dependencies = [
23
28
  testpaths = [
24
29
  "tests/*",
25
30
  ]
31
+ asyncio_mode = "auto"
@@ -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)
File without changes
File without changes