From f0b3e1417f81a9c4b474d10661270a47d9c8c7e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hidefj=C3=A4ll?= <50925050+dojje@users.noreply.github.com> Date: Sun, 19 Jul 2026 11:04:51 +0200 Subject: [PATCH 1/5] refactor: extract shared param builders to _params.py --- blocket_api/_params.py | 176 +++++++++++++++++++++++++++++++++++++++++ blocket_api/blocket.py | 158 +++++++++++++++--------------------- 2 files changed, 238 insertions(+), 96 deletions(-) create mode 100644 blocket_api/_params.py diff --git a/blocket_api/_params.py b/blocket_api/_params.py new file mode 100644 index 0000000..4890a8d --- /dev/null +++ b/blocket_api/_params.py @@ -0,0 +1,176 @@ +from __future__ import annotations + +from collections.abc import Sequence +from typing import Any + +from .constants import ( + SITE_URL, + 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, +) -> tuple[str, list[tuple[str, _ParamValue]]]: + if category and sub_category: + raise AssertionError("Cannot specify both category and sub_categories") + + url = f"{SITE_URL}/recommerce/forsale/search/api/search/SEARCH_ID_BAP_COMMON" + + 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 url, _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, +) -> tuple[str, list[tuple[str, _ParamValue]]]: + url = f"{SITE_URL}/mobility/search/api/search/SEARCH_ID_CAR_USED" + + 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 url, _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, +) -> tuple[str, list[tuple[str, _ParamValue]]]: + url = f"{SITE_URL}/mobility/search/api/search/SEARCH_ID_BOAT_USED" + + 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 url, _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, +) -> tuple[str, list[tuple[str, _ParamValue]]]: + url = f"{SITE_URL}/mobility/search/api/search/SEARCH_ID_MC_USED" + + 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 url, _build_params(defaults, extras) diff --git a/blocket_api/blocket.py b/blocket_api/blocket.py index 2f8fda4..913cf77 100644 --- a/blocket_api/blocket.py +++ b/blocket_api/blocket.py @@ -1,15 +1,19 @@ from __future__ import annotations -from dataclasses import dataclass from typing import Any import httpx -from httpx import Response +from ._params import ( + _ParamValue, + build_boat_params, + build_car_params, + build_mc_params, + build_search_params, +) from .ad_parser import BoatAd, CarAd, McAd, RecommerceAd +from .constants import HEADERS from .constants import ( - HEADERS, - SITE_URL, BoatType, CarColor, CarModel, @@ -26,23 +30,12 @@ ) -@dataclass(frozen=True) -class QueryParam: - name: str - value: str | int - - -def _request(*, url: str, params: list[QueryParam]) -> Response: - response = httpx.get( - url, - headers=HEADERS, - params=[(param.name, param.value) for param in params], - ) +def _request(*, url: str, params: list[tuple[str, _ParamValue]]) -> httpx.Response: + response = httpx.get(url, headers=HEADERS, params=params) response.raise_for_status() return response -@dataclass(frozen=True) class BlocketAPI: def search( self, @@ -54,23 +47,14 @@ def search( category: Category | None = None, sub_category: SubCategory | None = None, ) -> dict[str, Any]: - if category and sub_category: - raise AssertionError("Cannot specify both category and sub_categories") - - url = f"{SITE_URL}/recommerce/forsale/search/api/search/SEARCH_ID_BAP_COMMON" - - param_dict: dict[str, str | int | None] = { - "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, - } - - params = [QueryParam(k, v) for k, v in param_dict.items() if v is not None] - - params.extend(QueryParam("location", loc.value) for loc in locations) - + url, params = build_search_params( + query, + page=page, + sort_order=sort_order, + locations=locations, + category=category, + sub_category=sub_category, + ) return _request(url=url, params=params).json() def search_car( @@ -94,31 +78,25 @@ def search_car( wheel_drive: list[CarWheelDrive] = [], org_id: int | None = None, ) -> dict[str, Any]: - url = f"{SITE_URL}/mobility/search/api/search/SEARCH_ID_CAR_USED" - - param_dict: dict[str, str | int | None] = { - "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, - } - - params = [QueryParam(k, v) for k, v in param_dict.items() if v is not None] - - params.extend(QueryParam("location", loc.value) for loc in locations) - params.extend(QueryParam("make", model.value) for model in models) - params.extend(QueryParam("exterior_colour", color.value) for color in colors) - params.extend(QueryParam("transmission", t.value) for t in transmissions) - params.extend(QueryParam("wheel_drive", w.value) for w in wheel_drive) - + url, 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, + ) return _request(url=url, params=params).json() def search_boat( @@ -135,24 +113,18 @@ def search_boat( length_to: int | None = None, org_id: int | None = None, ) -> Any: - url = f"{SITE_URL}/mobility/search/api/search/SEARCH_ID_BOAT_USED" - - param_dict: dict[str, str | int | None] = { - "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, - } - - params = [QueryParam(k, v) for k, v in param_dict.items() if v is not None] - - params.extend(QueryParam("class", t.value) for t in types) - params.extend(QueryParam("location", loc.value) for loc in locations) - + url, 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, + ) return _request(url=url, params=params).json() def search_mc( @@ -170,25 +142,19 @@ def search_mc( engine_volume_to: int | None = None, org_id: int | None = None, ) -> dict[str, Any]: - url = f"{SITE_URL}/mobility/search/api/search/SEARCH_ID_MC_USED" - - param_dict: dict[str, str | int | None] = { - "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, - } - - params = [QueryParam(k, v) for k, v in param_dict.items() if v is not None] - - params.extend(QueryParam("make", m.value) for m in models) - params.extend(QueryParam("location", loc.value) for loc in locations) - params.extend(QueryParam("type", t.value) for t in types) - + url, 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, + ) return _request(url=url, params=params).json() def get_ad(self, ad: RecommerceAd | CarAd | BoatAd | McAd) -> dict[str, Any]: From 0065d257c0d664ddff22cda7ecaceb1a30c24c59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hidefj=C3=A4ll?= <50925050+dojje@users.noreply.github.com> Date: Sun, 19 Jul 2026 11:04:54 +0200 Subject: [PATCH 2/5] feat: add AsyncBlocketAPI for async/await support --- blocket_api/__init__.py | 2 + blocket_api/async_blocket.py | 179 ++++++++++++++++++ pyproject.toml | 2 + tests/async_requests.py | 356 +++++++++++++++++++++++++++++++++++ uv.lock | 25 +++ 5 files changed, 564 insertions(+) create mode 100644 blocket_api/async_blocket.py create mode 100644 tests/async_requests.py diff --git a/blocket_api/__init__.py b/blocket_api/__init__.py index 85717ff..2b28071 100644 --- a/blocket_api/__init__.py +++ b/blocket_api/__init__.py @@ -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, @@ -17,6 +18,7 @@ ) __all__ = [ + "AsyncBlocketAPI", "BlocketAPI", "Location", "BoatAd", diff --git a/blocket_api/async_blocket.py b/blocket_api/async_blocket.py new file mode 100644 index 0000000..fda3221 --- /dev/null +++ b/blocket_api/async_blocket.py @@ -0,0 +1,179 @@ +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 HEADERS +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]: + url, params = build_search_params( + query, + page=page, + sort_order=sort_order, + locations=locations, + category=category, + sub_category=sub_category, + ) + response = await self._client.get(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]: + url, 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(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: + url, 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(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]: + url, 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(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) diff --git a/pyproject.toml b/pyproject.toml index 1a0a9aa..ce4ee26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ dependencies = [ dev = [ "pre-commit>=4.5.1", "pytest>=9.0.2", + "pytest-asyncio>=0.26.0", "respx>=0.22.0", "mypy>=1.19.1", "ruff>=0.9.0", @@ -27,3 +28,4 @@ dev = [ testpaths = [ "tests/*", ] +asyncio_mode = "auto" diff --git a/tests/async_requests.py b/tests/async_requests.py new file mode 100644 index 0000000..84229b5 --- /dev/null +++ b/tests/async_requests.py @@ -0,0 +1,356 @@ +import httpx +import respx + +from blocket_api import ( + AsyncBlocketAPI, + BoatType, + CarAd, + CarModel, + CarSortOrder, + CarTransmission, + CarWheelDrive, + Category, + Location, + McAd, + McModel, + McType, + RecommerceAd, + SortOrder, + SubCategory, +) +from blocket_api.constants import SITE_URL + + +class Test_AsyncSearch: + @respx.mock + async def test_search(self) -> None: + expected_url = ( + f"{SITE_URL}/recommerce/forsale/search/api/search/SEARCH_ID_BAP_COMMON" + "?q=audi+q5" + "&page=1" + "&sort=RELEVANCE" + ) + respx.get(expected_url).mock( + return_value=httpx.Response(200, json={"status": "ok"}) + ) + async with AsyncBlocketAPI() as api: + result = await api.search("audi q5") + assert result == {"status": "ok"} + + @respx.mock + async def test_search_sortorder_published_desc(self) -> None: + expected_url = ( + f"{SITE_URL}/recommerce/forsale/search/api/search/SEARCH_ID_BAP_COMMON" + "?q=audi+q5" + "&page=1" + "&sort=PUBLISHED_DESC" + ) + respx.get(expected_url).mock( + return_value=httpx.Response(200, json={"status": "ok"}) + ) + async with AsyncBlocketAPI() as api: + result = await api.search( + "audi q5", sort_order=SortOrder.PUBLISHED_DESC + ) + assert result == {"status": "ok"} + + @respx.mock + async def test_search_location(self) -> None: + expected_url = ( + f"{SITE_URL}/recommerce/forsale/search/api/search/SEARCH_ID_BAP_COMMON" + "?q=audi+q5" + "&page=1" + "&sort=PUBLISHED_DESC" + "&location=0.300003" + "&location=0.300001" + ) + respx.get(expected_url).mock( + return_value=httpx.Response(200, json={"status": "ok"}) + ) + async with AsyncBlocketAPI() as api: + result = await api.search( + "audi q5", + sort_order=SortOrder.PUBLISHED_DESC, + locations=[Location.UPPSALA, Location.STOCKHOLM], + ) + assert result == {"status": "ok"} + + @respx.mock + async def test_search_category(self) -> None: + expected_url = ( + f"{SITE_URL}/recommerce/forsale/search/api/search/SEARCH_ID_BAP_COMMON" + "?q=audi+q5" + "&page=1" + "&sort=PUBLISHED_DESC" + "&category=0.90" + ) + respx.get(expected_url).mock( + return_value=httpx.Response(200, json={"status": "ok"}) + ) + async with AsyncBlocketAPI() as api: + result = await api.search( + "audi q5", + sort_order=SortOrder.PUBLISHED_DESC, + category=Category.FORDONSTILLBEHOR, + ) + assert result == {"status": "ok"} + + @respx.mock + async def test_sub_category(self) -> None: + expected_url = ( + f"{SITE_URL}/recommerce/forsale/search/api/search/SEARCH_ID_BAP_COMMON" + "?q=hammare" + "&page=1" + "&sort=RELEVANCE" + "&sub_category=1.67.5219" + ) + respx.get(expected_url).mock( + return_value=httpx.Response(200, json={"status": "ok"}) + ) + async with AsyncBlocketAPI() as api: + result = await api.search( + "hammare", + sub_category=SubCategory.VERKTYG, + ) + assert result == {"status": "ok"} + + +class Test_AsyncSearchCar: + @respx.mock + async def test_search_car(self) -> None: + expected_url = ( + f"{SITE_URL}/mobility/search/api/search/SEARCH_ID_CAR_USED" + "?sort=MILEAGE_ASC" + "&page=1" + "&make=0.744" + "&price_from=1000" + "&price_to=50000" + "&transmission=2" + "&wheel_drive=3" + "&engine_effect_from=200" + "&engine_effect_to=300" + "&orgId=1337" + ) + respx.get(expected_url).mock( + return_value=httpx.Response(200, json={"status": "ok"}) + ) + async with AsyncBlocketAPI() as api: + result = await api.search_car( + sort_order=CarSortOrder.MILEAGE_ASC, + models=[CarModel.AUDI], + price_from=1000, + price_to=50000, + transmissions=[CarTransmission.AUTOMATIC], + wheel_drive=[CarWheelDrive.FWD], + horsepower_from=200, + horsepower_to=300, + org_id=1337, + ) + assert result == {"status": "ok"} + + +class Test_AsyncSearchBoat: + @respx.mock + async def test_search_boat(self) -> None: + expected_url = ( + f"{SITE_URL}/mobility/search/api/search/SEARCH_ID_BOAT_USED" + "?q=Mercury" + "&page=1" + "&sort=RELEVANCE" + "&class=2184" + "&location=0.300001" + "&price_from=20000" + "&price_to=90000" + "&length_feet_from=10" + "&length_feet_to=15" + "&orgId=1337" + ) + respx.get(expected_url).mock( + return_value=httpx.Response(200, json={"status": "ok"}) + ) + async with AsyncBlocketAPI() as api: + result = await api.search_boat( + "Mercury", + types=[BoatType.DAYCRUISER], + locations=[Location.STOCKHOLM], + length_from=10, + length_to=15, + price_from=20000, + price_to=90000, + org_id=1337, + ) + assert result == {"status": "ok"} + + +class Test_AsyncMcSearch: + @respx.mock + async def test_search_mc(self) -> None: + expected_url = ( + f"{SITE_URL}/mobility/search/api/search/SEARCH_ID_MC_USED" + "?q=snabb" + "&page=1" + "&sort=RELEVANCE" + "&price_from=20000" + "&price_to=90000" + "&engine_volume_from=10" + "&engine_volume_to=15" + "&make=1484" + "&location=0.300001" + "&type=11" + "&orgId=1337" + ) + respx.get(expected_url).mock( + return_value=httpx.Response(200, json={"status": "ok"}) + ) + async with AsyncBlocketAPI() as api: + result = await api.search_mc( + "snabb", + types=[McType.SPORT], + locations=[Location.STOCKHOLM], + models=[McModel.DUCATI], + price_from=20000, + price_to=90000, + engine_volume_from=10, + engine_volume_to=15, + org_id=1337, + ) + assert result == {"status": "ok"} + + +class Test_AsyncGetAd: + @respx.mock + async def test_get_ad_recommerce(self) -> None: + expected_url = f"{SITE_URL}/recommerce/forsale/item/12345567" + respx.get(expected_url).mock( + return_value=httpx.Response( + 200, + content=b'', + ) + ) + async with AsyncBlocketAPI() as api: + result = await api.get_ad(RecommerceAd(12345567)) + assert result == {"some": "json here"} + + @respx.mock + async def test_get_ad_car(self) -> None: + expected_url = f"{SITE_URL}/mobility/item/123456" + html = """ +
D4 AWD Momentum
+ +2020
+4500
+Pris
+ 389 000 kr +Annons-ID
+123456
+Snabb båge
+ +2020
+4500
+150 cc
+Pris
+ 110 000 kr +Annons-ID
+123456
+