Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,19 @@ api.get_ad(BoatAd(12345678))
api.get_ad(McAd(12345678))
```

### Async support

```py
import asyncio
from blocket_api import AsyncBlocketAPI

async def main():
async with AsyncBlocketAPI() as api:
results = await api.search("iPhone 15")

asyncio.run(main())
```

## 📝 Notes

- REST API: https://blocket-api.se
Expand Down
2 changes: 2 additions & 0 deletions blocket_api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .ad_parser import BoatAd, CarAd, McAd, RecommerceAd
from .async_blocket import AsyncBlocketAPI
from .blocket import BlocketAPI, Location
from .constants import (
BoatSortOrder,
Expand All @@ -17,6 +18,7 @@
)

__all__ = [
"AsyncBlocketAPI",
"BlocketAPI",
"Location",
"BoatAd",
Expand Down
166 changes: 166 additions & 0 deletions blocket_api/_params.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
from __future__ import annotations

from collections.abc import Sequence

from .constants import (
BoatType,
CarColor,
CarModel,
CarSortOrder,
CarTransmission,
CarWheelDrive,
Category,
Location,
McModel,
McSortOrder,
McType,
SortOrder,
SubCategory,
)

_ParamValue = str | int | float | bool | None


def _build_params(
defaults: dict[str, _ParamValue],
extras: Sequence[tuple[str, _ParamValue]],
) -> list[tuple[str, _ParamValue]]:
params: list[tuple[str, _ParamValue]] = [
(k, v) for k, v in defaults.items() if v is not None
]
params.extend(extras)
return params


def build_search_params(
query: str,
*,
page: int = 1,
sort_order: SortOrder = SortOrder.RELEVANCE,
locations: list[Location] = [],
category: Category | None = None,
sub_category: SubCategory | None = None,
) -> list[tuple[str, _ParamValue]]:
if category and sub_category:
raise AssertionError("Cannot specify both category and sub_categories")

defaults: dict[str, _ParamValue] = {
"q": query,
"page": page,
"sort": sort_order.value,
"category": category.value if category else None,
"sub_category": sub_category.value if sub_category else None,
}

extras = [("location", loc.value) for loc in locations]

return _build_params(defaults, extras)


def build_car_params(
query: str | None = None,
*,
page: int = 1,
sort_order: CarSortOrder = CarSortOrder.RELEVANCE,
locations: list[Location] = [],
models: list[CarModel] = [],
price_from: int | None = None,
price_to: int | None = None,
year_from: int | None = None,
year_to: int | None = None,
milage_from: int | None = None,
milage_to: int | None = None,
colors: list[CarColor] = [],
transmissions: list[CarTransmission] = [],
horsepower_from: int | None = None,
horsepower_to: int | None = None,
wheel_drive: list[CarWheelDrive] = [],
org_id: int | None = None,
) -> list[tuple[str, _ParamValue]]:
defaults: dict[str, _ParamValue] = {
"q": query,
"page": page,
"sort": sort_order.value,
"price_from": price_from,
"price_to": price_to,
"year_from": year_from,
"year_to": year_to,
"milage_from": milage_from,
"milage_to": milage_to,
"engine_effect_from": horsepower_from,
"engine_effect_to": horsepower_to,
"orgId": org_id,
}

extras: list[tuple[str, _ParamValue]] = []
extras.extend(("location", loc.value) for loc in locations)
extras.extend(("make", model.value) for model in models)
extras.extend(("exterior_colour", color.value) for color in colors)
extras.extend(("transmission", t.value) for t in transmissions)
extras.extend(("wheel_drive", w.value) for w in wheel_drive)

return _build_params(defaults, extras)


def build_boat_params(
query: str | None = None,
*,
page: int = 1,
sort_order: CarSortOrder = CarSortOrder.RELEVANCE,
types: list[BoatType] = [],
locations: list[Location] = [],
price_from: int | None = None,
price_to: int | None = None,
length_from: int | None = None,
length_to: int | None = None,
org_id: int | None = None,
) -> list[tuple[str, _ParamValue]]:
defaults: dict[str, _ParamValue] = {
"q": query,
"page": page,
"sort": sort_order.value,
"price_from": price_from,
"price_to": price_to,
"length_feet_from": length_from,
"length_feet_to": length_to,
"orgId": org_id,
}

extras: list[tuple[str, _ParamValue]] = []
extras.extend(("class", t.value) for t in types)
extras.extend(("location", loc.value) for loc in locations)

return _build_params(defaults, extras)


def build_mc_params(
query: str | None = None,
*,
page: int = 1,
sort_order: McSortOrder = McSortOrder.RELEVANCE,
models: list[McModel] = [],
types: list[McType] = [],
locations: list[Location] = [],
price_from: int | None = None,
price_to: int | None = None,
engine_volume_from: int | None = None,
engine_volume_to: int | None = None,
org_id: int | None = None,
) -> list[tuple[str, _ParamValue]]:
defaults: dict[str, _ParamValue] = {
"q": query,
"page": page,
"sort": sort_order.value,
"price_from": price_from,
"price_to": price_to,
"engine_volume_from": engine_volume_from,
"engine_volume_to": engine_volume_to,
"orgId": org_id,
}

extras: list[tuple[str, _ParamValue]] = []
extras.extend(("make", m.value) for m in models)
extras.extend(("location", loc.value) for loc in locations)
extras.extend(("type", t.value) for t in types)

return _build_params(defaults, extras)
187 changes: 187 additions & 0 deletions blocket_api/async_blocket.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
from __future__ import annotations

from typing import Any

import httpx

from ._params import (
build_boat_params,
build_car_params,
build_mc_params,
build_search_params,
)
from .ad_parser import BoatAd, CarAd, McAd, RecommerceAd
from .constants import (
BOAT_SEARCH_URL,
CAR_SEARCH_URL,
HEADERS,
MC_SEARCH_URL,
SEARCH_URL,
)
from .constants import (
BoatType,
CarColor,
CarModel,
CarSortOrder,
CarTransmission,
CarWheelDrive,
Category,
Location,
McModel,
McSortOrder,
McType,
SortOrder,
SubCategory,
)


class AsyncBlocketAPI:
def __init__(self, client: httpx.AsyncClient | None = None) -> None:
self._client = client or httpx.AsyncClient()
self._owns_client = client is None

async def __aenter__(self) -> AsyncBlocketAPI:
return self

async def __aexit__(self, *_: Any) -> None:
await self.aclose()

async def aclose(self) -> None:
await self._client.aclose()

async def search(
self,
query: str,
*,
page: int = 1,
sort_order: SortOrder = SortOrder.RELEVANCE,
locations: list[Location] = [],
category: Category | None = None,
sub_category: SubCategory | None = None,
) -> dict[str, Any]:
params = build_search_params(
query,
page=page,
sort_order=sort_order,
locations=locations,
category=category,
sub_category=sub_category,
)
response = await self._client.get(SEARCH_URL, headers=HEADERS, params=params)
response.raise_for_status()
return response.json()

async def search_car(
self,
query: str | None = None,
*,
page: int = 1,
sort_order: CarSortOrder = CarSortOrder.RELEVANCE,
locations: list[Location] = [],
models: list[CarModel] = [],
price_from: int | None = None,
price_to: int | None = None,
year_from: int | None = None,
year_to: int | None = None,
milage_from: int | None = None,
milage_to: int | None = None,
colors: list[CarColor] = [],
transmissions: list[CarTransmission] = [],
horsepower_from: int | None = None,
horsepower_to: int | None = None,
wheel_drive: list[CarWheelDrive] = [],
org_id: int | None = None,
) -> dict[str, Any]:
params = build_car_params(
query,
page=page,
sort_order=sort_order,
locations=locations,
models=models,
price_from=price_from,
price_to=price_to,
year_from=year_from,
year_to=year_to,
milage_from=milage_from,
milage_to=milage_to,
colors=colors,
transmissions=transmissions,
horsepower_from=horsepower_from,
horsepower_to=horsepower_to,
wheel_drive=wheel_drive,
org_id=org_id,
)
response = await self._client.get(
CAR_SEARCH_URL, headers=HEADERS, params=params
)
response.raise_for_status()
return response.json()

async def search_boat(
self,
query: str | None = None,
*,
page: int = 1,
sort_order: CarSortOrder = CarSortOrder.RELEVANCE,
types: list[BoatType] = [],
locations: list[Location] = [],
price_from: int | None = None,
price_to: int | None = None,
length_from: int | None = None,
length_to: int | None = None,
org_id: int | None = None,
) -> Any:
params = build_boat_params(
query,
page=page,
sort_order=sort_order,
types=types,
locations=locations,
price_from=price_from,
price_to=price_to,
length_from=length_from,
length_to=length_to,
org_id=org_id,
)
response = await self._client.get(
BOAT_SEARCH_URL, headers=HEADERS, params=params
)
response.raise_for_status()
return response.json()

async def search_mc(
self,
query: str | None = None,
*,
page: int = 1,
sort_order: McSortOrder = McSortOrder.RELEVANCE,
models: list[McModel] = [],
types: list[McType] = [],
locations: list[Location] = [],
price_from: int | None = None,
price_to: int | None = None,
engine_volume_from: int | None = None,
engine_volume_to: int | None = None,
org_id: int | None = None,
) -> dict[str, Any]:
params = build_mc_params(
query,
page=page,
sort_order=sort_order,
models=models,
types=types,
locations=locations,
price_from=price_from,
price_to=price_to,
engine_volume_from=engine_volume_from,
engine_volume_to=engine_volume_to,
org_id=org_id,
)
response = await self._client.get(MC_SEARCH_URL, headers=HEADERS, params=params)
response.raise_for_status()
return response.json()

async def get_ad(self, ad: RecommerceAd | CarAd | BoatAd | McAd) -> dict[str, Any]:
response = await self._client.get(ad.url, headers=HEADERS)
response.raise_for_status()
return ad.parse(response)
Loading
Loading