├── .envrc ├── .github └── workflows │ ├── publish.yml │ └── tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── ba.code-workspace ├── blocket_api ├── __init__.py └── blocket.py ├── default.nix ├── flake.lock ├── flake.nix ├── poetry.lock ├── pyproject.toml └── tests ├── __init__.py ├── assertions.py ├── requests.py └── searches.py /.envrc: -------------------------------------------------------------------------------- 1 | use_flake . 2 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload BlocketAPI to PyPI upon release. 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | publish: 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | python-version: [3.12] 13 | poetry-version: [1.8.3] 14 | os: [ubuntu-latest] 15 | runs-on: ${{ matrix.os }} 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: actions/setup-python@v2 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | - name: Run image 22 | uses: abatilo/actions-poetry@v2.0.0 23 | with: 24 | poetry-version: ${{ matrix.poetry-version }} 25 | - name: Publish 26 | env: 27 | PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} 28 | run: | 29 | poetry config pypi-token.pypi $PYPI_TOKEN 30 | poetry publish --build 31 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Push 2 | on: [push] 3 | 4 | jobs: 5 | pytest: 6 | strategy: 7 | fail-fast: false 8 | matrix: 9 | python-version: [3.12] 10 | poetry-version: [1.8.3] 11 | os: [ubuntu-latest] 12 | runs-on: ${{ matrix.os }} 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-python@v2 16 | with: 17 | python-version: ${{ matrix.python-version }} 18 | - name: Run image 19 | uses: abatilo/actions-poetry@v2.0.0 20 | with: 21 | poetry-version: ${{ matrix.poetry-version }} 22 | - name: Install dependencies 23 | run: poetry install 24 | - name: Run tests 25 | run: poetry run pytest 26 | ruff: 27 | strategy: 28 | fail-fast: false 29 | matrix: 30 | python-version: [3.12] 31 | poetry-version: [1.8.3] 32 | os: [ubuntu-latest] 33 | runs-on: ${{ matrix.os }} 34 | steps: 35 | - uses: actions/checkout@v2 36 | - uses: actions/setup-python@v2 37 | with: 38 | python-version: ${{ matrix.python-version }} 39 | - name: Run image 40 | uses: abatilo/actions-poetry@v2.0.0 41 | with: 42 | poetry-version: ${{ matrix.poetry-version }} 43 | - name: Install dependencies 44 | run: poetry install 45 | - name: Check ruff 46 | run: poetry run ruff check . 47 | 48 | mypy: 49 | strategy: 50 | fail-fast: false 51 | matrix: 52 | python-version: [3.12] 53 | poetry-version: [1.8.3] 54 | os: [ubuntu-latest] 55 | runs-on: ${{ matrix.os }} 56 | steps: 57 | - uses: actions/checkout@v2 58 | - uses: actions/setup-python@v2 59 | with: 60 | python-version: ${{ matrix.python-version }} 61 | - name: Run image 62 | uses: abatilo/actions-poetry@v2.0.0 63 | with: 64 | poetry-version: ${{ matrix.poetry-version }} 65 | - name: Install dependencies 66 | run: poetry install 67 | - name: Check mypy 68 | run: poetry run mypy . --check-untyped-defs --disallow-untyped-defs --ignore-missing-imports -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .direnv 2 | .env 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | checker/__pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | .pybuilder/ 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # poetry 102 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 103 | # This is especially recommended for binary packages to ensure reproducibility, and is more 104 | # commonly ignored for libraries. 105 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 106 | #poetry.lock 107 | 108 | # pdm 109 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 110 | #pdm.lock 111 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 112 | # in version control. 113 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 114 | .pdm.toml 115 | .pdm-python 116 | .pdm-build/ 117 | 118 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 119 | __pypackages__/ 120 | 121 | # Celery stuff 122 | celerybeat-schedule 123 | celerybeat.pid 124 | 125 | # SageMath parsed files 126 | *.sage.py 127 | 128 | # Environments 129 | .venv 130 | env/ 131 | venv/ 132 | ENV/ 133 | env.bak/ 134 | venv.bak/ 135 | 136 | # Spyder project settings 137 | .spyderproject 138 | .spyproject 139 | 140 | # Rope project settings 141 | .ropeproject 142 | 143 | # mkdocs documentation 144 | /site 145 | 146 | # mypy 147 | .mypy_cache/ 148 | .dmypy.json 149 | dmypy.json 150 | 151 | # Pyre type checker 152 | .pyre/ 153 | 154 | # pytype static type analyzer 155 | .pytype/ 156 | 157 | # Cython debug symbols 158 | cython_debug/ 159 | 160 | # PyCharm 161 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 162 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 163 | # and can be added to the global gitignore or merged into this file. For a more nuclear 164 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 165 | #.idea/ 166 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: local 3 | hooks: 4 | - id: ruff-format 5 | name: ruff-format 6 | entry: ruff format --no-cache 7 | language: system 8 | types_or: [python] 9 | - id: ruff-check 10 | name: ruff-check 11 | entry: ruff check --fix --no-cache 12 | language: system 13 | types: [python] 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2018 Satwik Kansal 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BlocketAPI 2 | 3 | [![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 versions](https://img.shields.io/pypi/pyversions/blocket-api?style=for-the-badge)](https://pypi.org/project/blocket_api/) ![PyPI - Downloads](https://img.shields.io/pypi/dm/blocket_api?style=for-the-badge&color=%23dbce58) 4 | 5 | BlocketAPI allows users to query saved searches, known as "Bevakningar", on [blocket.se](https://blocket.se/). This means you can either retrieve results from a specific saved search or list all listings/ads across all saved searches. The results from these queries are returned in a `json` format. 6 | 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. 8 | 9 | ## ✨ Features 10 | 11 | - List saved searches, called "Bevakningar". 12 | - Query all listings/ads filtered on a region. 13 | - Query listings related to a saved search. 14 | - Use motor search to query listings related to a specific car. 15 | 16 | ## 🧑‍💻️ Install 17 | 18 | BlocketAPI is available on PyPI. 19 | 20 | ```sh 21 | pip install blocket-api 22 | ``` 23 | 24 | ## 💁‍♀️ Usage 25 | 26 | ```py 27 | >>> from blocket_api import BlocketAPI 28 | >>> api = BlocketAPI("YourBlocketTokenHere") 29 | >>> print(api.saved_searches()) 30 | ... 31 | >>> print(BlocketAPI().custom_search("saab")) # no token required 32 | ... 33 | ``` 34 | 35 | Some calls require a bearerToken. However, some calls are public and don't require a token. 36 | 37 | [Where token?](#-blocket-api-token) 38 | 39 | 40 | | Function | Token required | Description | 41 | |---|---|---| 42 | | `api.saved_searches()` | 🔐 Yes | List your saved searches (bevakningar) | 43 | | `api.get_listings()` | 🔐 Yes | List items related to a saved search | 44 | | `api.custom_search()` | 👏 No | Search for everything on Blocket and filter by region | 45 | | `api.motor_search()` | 👏 No | Advanced search for car-listings. | 46 | 47 | ## 🤓 Detailed usage 48 | 49 | ### saved_searches() 50 | 51 | Saved searches are your so called "Bevakningar" and can be found [here](https://www.blocket.se/sparade/bevakningar). Each saved search has and unique `id` which can be used as a parameter to `get_listings()`, see below. 52 | 53 | ```py 54 | >>> api.saved_searches() 55 | [ 56 | { 57 | "id":"4150081", 58 | "new_count":0, 59 | "total_count":41, 60 | "push_enabled":false, 61 | "push_available":true, 62 | "query":"cg=1020&q=buggy&st=s", 63 | "name":"\"buggy\", Bilar säljes i hela Sverige" 64 | }, 65 | ] 66 | ``` 67 | 68 | ### get_listings(search_id, limit) 69 | Returns all listings related to a saved search. 70 | 71 | Parameters: 72 | - `search_id` (`int`, optional) - Get listings for a specific saved search. If not provided, all saved searches will be combined. 73 | - `limit` (`int`, optional) - Limit number of results returned, max is 99. 74 | 75 | ```py 76 | >>> api.get_listings(4150081) 77 | { 78 | "data":[ 79 | { 80 | "ad":{ 81 | "ad_id":"1401053984", 82 | "list_id":"1401053984", 83 | "zipcode":"81290", 84 | "ad_status":"active", 85 | "list_time":"2024-07-15T19:07:16+02:00", 86 | "subject":"Volkswagen 1500 lim 113 chassi", 87 | "body":"Säljer ett chassi/bottenplatta till en volkswagen 1500 lim 113 1967, blästrat och målat.\nFinns en beach buggy kaross att få med om man vill det. \nReg nmr ABC123", 88 | "price":{ 89 | "value":10000, 90 | "suffix":"kr" 91 | }, 92 | ... 93 | }, 94 | }, 95 | ], 96 | "total_count":41, 97 | "timestamp":"2024-07-16T08:08:43.810828006Z", 98 | "total_page_count":1 99 | } 100 | ``` 101 | 102 | ### custom_search(search_query, region, limit) 103 | Make a custom search through out all of blocked. A region can be passed in as parameter for filtering. 104 | 105 | Parameters: 106 | - `search_query` (`str`, required) - A string to search for. 107 | - `region` (`str`, optional) - Filter results on a region, default is all of Sweden. 108 | - `limit` (`int`, optional) - Limit number of results returned, max is 99. 109 | 110 | ```py 111 | >>> from blocket_api import Region 112 | >>> api.custom_search("saab", Region.blekinge) # search for term "saab" in region of "Blekinge" 113 | { 114 | "data":[ 115 | { 116 | "ad_id":"1401038836", 117 | "ad_status":"active", 118 | "advertiser":{ 119 | "account_id":"684279", 120 | "name":"Stefan Ingves", 121 | "type":"private" 122 | }, 123 | ... 124 | "location":[ 125 | { 126 | "id":"22", 127 | "name":"Blekinge", 128 | "query_key":"r" 129 | }, 130 | { 131 | "id":"256", 132 | "name":"Ronneby", 133 | "query_key":"m" 134 | } 135 | ], 136 | ... 137 | } 138 | ] 139 | } 140 | ``` 141 | 142 | ### motor_search(page, make, fuel, chassi, price, modelYear, milage, gearbox) 143 | To query listings related to a specific car, supply the following parameters: 144 | 145 | - `page` (`int`, required) - Results are split in pages, set page number here. 146 | - `make` (`List[MAKE_OPTIONS]`) - Filter a specific make, ex. `Audi`. 147 | - `fuel` (`Optional[List[FUEL_OPTIONS]]`) - Filter a specific fuel, ex. `Diesel`. 148 | - `chassi` (`Optional[List[CHASSI_OPTIONS]]`) - Filter a specific chassi, ex. `Cab`. 149 | - `price` (`Optional[Tuple[int, int]]`) - Set price range, ex. `(50000, 100000)`. 150 | - `modelYear` (`Optional[Tuple[int, int]]`) - Set model year range, ex. `(2000, 2020)`. 151 | - `milage` (`Optional[Tuple[int, int]]`) - Set milage range, ex. `(1000, 2000)`. 152 | - `gearbox` (`Optional[GEARBOX_OPTIONS]`) - Filter a specific gearbox, ex. `Automat`. 153 | ```py 154 | >>> api.motor_search( 155 | make=["Audi", "Ford"], 156 | fuel=["Diesel"], 157 | chassi=["Cab"], 158 | price=(50000, 100000), 159 | page=1, 160 | ) 161 | ... 162 | ``` 163 | 164 | ## 🔐 Blocket API token 165 | 166 | There are two ways to acquire your token: 167 | 168 | - Log in to Blocket.se with your credentials using any web browser. 169 | - Go to [this](https://www.blocket.se/api/adout-api-route/refresh-token-and-validate-session) URL and copy the value of `bearerToken`. 170 | 171 | If there's a better way of doing this, feel free to help out in [#2](https://github.com/dunderrrrrr/blocket_api/issues/2). 172 | 173 | Your token can also be found in the request headers in the "Bevakningar"-section on Blocket. 174 | 175 | - **Login to [blocket.se](https**://blocket.se/)**: Sign in with your credentials. 176 | - **Click "Bevakningar"**: Go to the "Bevakningar" section. 177 | - **Inspect the page**: Right-click the page and select "Inspect". 178 | - **Open the Network tab**: Switch to the Network tab in the Developer Tools. 179 | - **Find request headers**: Locate a request where the domain is "api.blocket.se" and the file is "searches". *Pretty much every request to api.blocket.se contains this auth-header, so any request will do.* 180 | - **Inspect request headers**: Look at the request headers to find your token under "Authorization". 181 | 182 | ![token](https://i.imgur.com/E5ofN0e.png) 183 | 184 | My token has never expired or changed during this project. However, if your're met with a `401 Unauthorized` at some point, you may want to refresh your token by repeating the steps above. 185 | 186 | ## 📝 Notes 187 | 188 | - Source repo: https://github.com/dunderrrrrr/blocket_api 189 | - PyPI: https://pypi.org/project/blocket-api/ 190 | -------------------------------------------------------------------------------- /ba.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": { 8 | "editor.formatOnSave": true, 9 | "python.formatting.provider": "black", 10 | "editor.defaultFormatter": "ms-python.black-formatter", 11 | "python.testing.autoTestDiscoverOnSaveEnabled": false, 12 | "python.testing.pytestEnabled": true, 13 | "python.linting.mypyEnabled": true, 14 | "python.linting.mypyPath": ".venv/bin/dmypy", 15 | "python.linting.mypyArgs": ["run", "--"], 16 | "python.linting.enabled": true, 17 | "python.linting.flake8Enabled": true, 18 | "python.linting.pylintEnabled": false, 19 | "[python]": { 20 | "editor.codeActionsOnSave": { 21 | "source.organizeImports": "explicit" 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /blocket_api/__init__.py: -------------------------------------------------------------------------------- 1 | from .blocket import BlocketAPI as BlocketAPI 2 | from .blocket import Region as Region 3 | -------------------------------------------------------------------------------- /blocket_api/blocket.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Callable 4 | from functools import wraps 5 | import urllib 6 | from dataclasses import dataclass 7 | from enum import Enum 8 | from typing import TYPE_CHECKING, Any, List, Literal, Optional, Tuple 9 | 10 | import httpx 11 | 12 | if TYPE_CHECKING: 13 | from httpx import Response 14 | 15 | BASE_URL = "https://api.blocket.se" 16 | SITE_URL = "https://www.blocket.se" 17 | 18 | 19 | class Region(Enum): 20 | hela_sverige = 0 21 | östergötland = 14 22 | blekinge = 22 23 | dalarna = 6 24 | gotland = 19 25 | gävleborg = 5 26 | göteborg = 15 27 | halland = 20 28 | jämtland = 3 29 | jönköping = 17 30 | kalmar = 18 31 | kronoberg = 21 32 | norrbotten = 1 33 | skaraborg = 13 34 | skåne = 23 35 | stockholm = 11 36 | södermanland = 12 37 | uppsala = 10 38 | värmland = 7 39 | västerbotten = 2 40 | västernorrland = 4 41 | västmanland = 9 42 | älvsborg = 16 43 | örebro = 8 44 | 45 | 46 | MAKE_OPTIONS = Literal[ 47 | "Audi", 48 | "BMW", 49 | "Chevrolet", 50 | "Citroën", 51 | "Ford", 52 | "Honda", 53 | "Hyundai", 54 | "Kia", 55 | "Mazda", 56 | "Mercedes-Benz", 57 | "Nissan", 58 | "Opel", 59 | "Peugeot", 60 | "Renault", 61 | "Saab", 62 | "Skoda", 63 | "Subaru", 64 | "Toyota", 65 | "Volkswagen", 66 | "Volvo", 67 | ] 68 | 69 | FUEL_OPTIONS = Literal["Diesel", "Bensin", "El", "Miljöbränsle/Hybrid"] 70 | CHASSI_OPTIONS = Literal[ 71 | "Kombi", "SUV", "Sedan", "Halvkombi", "Coupé", "Cab", "Familjebuss", "Yrkesfordon" 72 | ] 73 | GEARBOX_OPTIONS = Literal["Automat", "Manuell"] 74 | 75 | 76 | class APIError(Exception): ... 77 | 78 | 79 | class LimitError(Exception): ... 80 | 81 | 82 | class TokenError(Exception): ... 83 | 84 | 85 | def auth_token(method: Callable) -> Callable: 86 | @wraps(method) 87 | def wrapper(self: Any, *args: Any, **kwargs: Any) -> Callable: 88 | if not self.token: 89 | raise TokenError("Token is required, see documentation.") 90 | return method(self, *args, **kwargs) 91 | 92 | return wrapper 93 | 94 | 95 | def public_token(method: Callable) -> Callable: 96 | @wraps(method) 97 | def wrapper(self: Any, *args: Any, **kwargs: Any) -> Callable: 98 | if not self.token: 99 | response = httpx.get( 100 | f"{SITE_URL}/api/adout-api-route/refresh-token-and-validate-session" 101 | ) 102 | response.raise_for_status() 103 | self.token = response.json()["bearerToken"] 104 | return method(self, *args, **kwargs) 105 | 106 | return wrapper 107 | 108 | 109 | def _make_request(*, url: str, token: str, raise_for_status: bool = True) -> Response: 110 | headers = { 111 | "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0" 112 | } 113 | if token: 114 | headers["Authorization"] = f"Bearer {token}" 115 | try: 116 | response = httpx.get(url, headers=headers) 117 | if raise_for_status: 118 | response.raise_for_status() 119 | except Exception as E: 120 | raise APIError(E) 121 | return response 122 | 123 | 124 | @dataclass 125 | class BlocketAPI: 126 | token: str | None = None 127 | 128 | @auth_token 129 | def saved_searches(self) -> list[dict]: 130 | """ 131 | Retrieves saved searches data, also known as "Bevakningar". 132 | """ 133 | assert self.token 134 | 135 | searches = ( 136 | _make_request(url=f"{BASE_URL}/saved/v2/searches", token=self.token) 137 | .json() 138 | .get("data", []) 139 | ) 140 | mobility_searches = ( 141 | _make_request( 142 | url=f"{BASE_URL}/mobility-saved-searches/v1/searches", token=self.token 143 | ) 144 | .json() 145 | .get("data", []) 146 | ) 147 | 148 | return searches + mobility_searches 149 | 150 | def _for_search_id(self, search_id: int, limit: int) -> dict: 151 | assert self.token 152 | searches = _make_request( 153 | url=f"{BASE_URL}/saved/v2/searches_content/{search_id}?lim={limit}", 154 | token=self.token, 155 | raise_for_status=False, 156 | ) 157 | if searches.status_code == 404: 158 | mobility_searches = _make_request( 159 | url=f"{BASE_URL}/mobility-saved-searches/v1/searches/{search_id}/ads?lim={limit}", 160 | token=self.token, 161 | raise_for_status=True, 162 | ) 163 | return mobility_searches.json() 164 | return searches.json() 165 | 166 | @auth_token 167 | def get_listings(self, search_id: int | None = None, limit: int = 99) -> dict: 168 | """ 169 | Retrieve listings/ads based on the provided search criteria. 170 | """ 171 | assert self.token 172 | 173 | if limit > 99: 174 | raise LimitError("Limit cannot be greater than 99.") 175 | 176 | if search_id: 177 | return self._for_search_id(search_id, limit) 178 | 179 | return _make_request( 180 | url=BASE_URL + f"/saved/v2/searches_content?lim={limit}", 181 | token=self.token, 182 | ).json() 183 | 184 | @public_token 185 | def custom_search( 186 | self, search_query: str, region: Region = Region.hela_sverige, limit: int = 99 187 | ) -> dict: 188 | """ 189 | Do a custom search through out all of Blocket. 190 | Supply a region for filtering. Default is all of Sweden. 191 | """ 192 | assert self.token 193 | 194 | if limit > 99: 195 | raise LimitError("Limit cannot be greater than 99.") 196 | 197 | return _make_request( 198 | url=f"{BASE_URL}/search_bff/v2/content?lim={limit}&q={search_query}&r={region.value}&status=active", 199 | token=self.token, 200 | ).json() 201 | 202 | @public_token 203 | def motor_search( 204 | self, 205 | page: int, 206 | make: List[MAKE_OPTIONS], 207 | fuel: Optional[List[FUEL_OPTIONS]] = None, 208 | chassi: Optional[List[CHASSI_OPTIONS]] = None, 209 | price: Optional[Tuple[int, int]] = None, 210 | modelYear: Optional[Tuple[int, int]] = None, 211 | milage: Optional[Tuple[int, int]] = None, 212 | gearbox: Optional[GEARBOX_OPTIONS] = None, 213 | ) -> dict: 214 | """ 215 | Search specifically in the car section of Blocket 216 | with set optional parameters for filtering. 217 | """ 218 | assert self.token 219 | 220 | range_params = ["price", "modelYear", "milage"] 221 | set_params = { 222 | key: value 223 | for key, value in locals().items() 224 | if key not in ["self", "page", "range_params"] and value is not None 225 | } 226 | 227 | filters = [] 228 | for param, value in set_params.items(): 229 | filter = {"key": param, "values": value} 230 | if param in range_params: 231 | range_start, range_end = filter.pop("values") 232 | filter["range"] = { 233 | "start": str(range_start), 234 | "end": str(range_end), 235 | } 236 | filter_str = urllib.parse.quote(str(filter).replace("'", '"')) 237 | filters.append(filter_str) 238 | 239 | motor_base_url = f"{BASE_URL}/motor-search-service/v4/search/car" 240 | 241 | filters_str = "&".join([f"filter={f}" for f in filters]) 242 | url = f"{motor_base_url}?{filters_str}&page={page}" 243 | 244 | return _make_request(url=f"{url}", token=self.token).json() 245 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | {pkgs}: 2 | pkgs.poetry2nix.mkPoetryEnv { 3 | python = pkgs.python312; 4 | projectDir = ./.; 5 | editablePackageSources.playground = ./.; 6 | preferWheels = true; 7 | overrides = pkgs.poetry2nix.overrides.withDefaults (self: super: { 8 | ### package specific overrides go here... 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "alejandra": { 4 | "inputs": { 5 | "fenix": "fenix", 6 | "flakeCompat": "flakeCompat", 7 | "nixpkgs": [ 8 | "nixpkgs" 9 | ] 10 | }, 11 | "locked": { 12 | "lastModified": 1660592437, 13 | "narHash": "sha256-xFumnivtVwu5fFBOrTxrv6fv3geHKF04RGP23EsDVaI=", 14 | "owner": "kamadorueda", 15 | "repo": "alejandra", 16 | "rev": "e7eac49074b70814b542fee987af2987dd0520b5", 17 | "type": "github" 18 | }, 19 | "original": { 20 | "owner": "kamadorueda", 21 | "ref": "3.0.0", 22 | "repo": "alejandra", 23 | "type": "github" 24 | } 25 | }, 26 | "fenix": { 27 | "inputs": { 28 | "nixpkgs": [ 29 | "alejandra", 30 | "nixpkgs" 31 | ], 32 | "rust-analyzer-src": "rust-analyzer-src" 33 | }, 34 | "locked": { 35 | "lastModified": 1657607339, 36 | "narHash": "sha256-HaqoAwlbVVZH2n4P3jN2FFPMpVuhxDy1poNOR7kzODc=", 37 | "owner": "nix-community", 38 | "repo": "fenix", 39 | "rev": "b814c83d9e6aa5a28d0cf356ecfdafb2505ad37d", 40 | "type": "github" 41 | }, 42 | "original": { 43 | "owner": "nix-community", 44 | "repo": "fenix", 45 | "type": "github" 46 | } 47 | }, 48 | "flake-parts": { 49 | "inputs": { 50 | "nixpkgs-lib": "nixpkgs-lib" 51 | }, 52 | "locked": { 53 | "lastModified": 1719994518, 54 | "narHash": "sha256-pQMhCCHyQGRzdfAkdJ4cIWiw+JNuWsTX7f0ZYSyz0VY=", 55 | "owner": "hercules-ci", 56 | "repo": "flake-parts", 57 | "rev": "9227223f6d922fee3c7b190b2cc238a99527bbb7", 58 | "type": "github" 59 | }, 60 | "original": { 61 | "id": "flake-parts", 62 | "type": "indirect" 63 | } 64 | }, 65 | "flake-utils": { 66 | "inputs": { 67 | "systems": "systems" 68 | }, 69 | "locked": { 70 | "lastModified": 1710146030, 71 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", 72 | "owner": "numtide", 73 | "repo": "flake-utils", 74 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", 75 | "type": "github" 76 | }, 77 | "original": { 78 | "owner": "numtide", 79 | "repo": "flake-utils", 80 | "type": "github" 81 | } 82 | }, 83 | "flakeCompat": { 84 | "flake": false, 85 | "locked": { 86 | "lastModified": 1650374568, 87 | "narHash": "sha256-Z+s0J8/r907g149rllvwhb4pKi8Wam5ij0st8PwAh+E=", 88 | "owner": "edolstra", 89 | "repo": "flake-compat", 90 | "rev": "b4a34015c698c7793d592d66adbab377907a2be8", 91 | "type": "github" 92 | }, 93 | "original": { 94 | "owner": "edolstra", 95 | "repo": "flake-compat", 96 | "type": "github" 97 | } 98 | }, 99 | "nix-github-actions": { 100 | "inputs": { 101 | "nixpkgs": [ 102 | "poetry2nix", 103 | "nixpkgs" 104 | ] 105 | }, 106 | "locked": { 107 | "lastModified": 1703863825, 108 | "narHash": "sha256-rXwqjtwiGKJheXB43ybM8NwWB8rO2dSRrEqes0S7F5Y=", 109 | "owner": "nix-community", 110 | "repo": "nix-github-actions", 111 | "rev": "5163432afc817cf8bd1f031418d1869e4c9d5547", 112 | "type": "github" 113 | }, 114 | "original": { 115 | "owner": "nix-community", 116 | "repo": "nix-github-actions", 117 | "type": "github" 118 | } 119 | }, 120 | "nixpkgs": { 121 | "locked": { 122 | "lastModified": 0, 123 | "narHash": "sha256-EYekUHJE2gxeo2pM/zM9Wlqw1Uw2XTJXOSAO79ksc4Y=", 124 | "path": "/nix/store/qmh8bas1qni03drm0lnjas2azh7h87cn-source", 125 | "type": "path" 126 | }, 127 | "original": { 128 | "id": "nixpkgs", 129 | "type": "indirect" 130 | } 131 | }, 132 | "nixpkgs-lib": { 133 | "locked": { 134 | "lastModified": 1719876945, 135 | "narHash": "sha256-Fm2rDDs86sHy0/1jxTOKB1118Q0O3Uc7EC0iXvXKpbI=", 136 | "type": "tarball", 137 | "url": "https://github.com/NixOS/nixpkgs/archive/5daf0514482af3f97abaefc78a6606365c9108e2.tar.gz" 138 | }, 139 | "original": { 140 | "type": "tarball", 141 | "url": "https://github.com/NixOS/nixpkgs/archive/5daf0514482af3f97abaefc78a6606365c9108e2.tar.gz" 142 | } 143 | }, 144 | "poetry2nix": { 145 | "inputs": { 146 | "flake-utils": "flake-utils", 147 | "nix-github-actions": "nix-github-actions", 148 | "nixpkgs": [ 149 | "nixpkgs" 150 | ], 151 | "systems": "systems_2", 152 | "treefmt-nix": "treefmt-nix" 153 | }, 154 | "locked": { 155 | "lastModified": 1721010580, 156 | "narHash": "sha256-qxN9it4uicRKdEjKlSt3BvXC+mWgLlJHNwMztSQsQsE=", 157 | "owner": "nix-community", 158 | "repo": "poetry2nix", 159 | "rev": "7f304a86324aea2026e65e508c82af7127f9b00d", 160 | "type": "github" 161 | }, 162 | "original": { 163 | "owner": "nix-community", 164 | "repo": "poetry2nix", 165 | "type": "github" 166 | } 167 | }, 168 | "root": { 169 | "inputs": { 170 | "alejandra": "alejandra", 171 | "flake-parts": "flake-parts", 172 | "nixpkgs": "nixpkgs", 173 | "poetry2nix": "poetry2nix", 174 | "systems": "systems_3" 175 | } 176 | }, 177 | "rust-analyzer-src": { 178 | "flake": false, 179 | "locked": { 180 | "lastModified": 1657557289, 181 | "narHash": "sha256-PRW+nUwuqNTRAEa83SfX+7g+g8nQ+2MMbasQ9nt6+UM=", 182 | "owner": "rust-lang", 183 | "repo": "rust-analyzer", 184 | "rev": "caf23f29144b371035b864a1017dbc32573ad56d", 185 | "type": "github" 186 | }, 187 | "original": { 188 | "owner": "rust-lang", 189 | "ref": "nightly", 190 | "repo": "rust-analyzer", 191 | "type": "github" 192 | } 193 | }, 194 | "systems": { 195 | "locked": { 196 | "lastModified": 1681028828, 197 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 198 | "owner": "nix-systems", 199 | "repo": "default", 200 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 201 | "type": "github" 202 | }, 203 | "original": { 204 | "owner": "nix-systems", 205 | "repo": "default", 206 | "type": "github" 207 | } 208 | }, 209 | "systems_2": { 210 | "locked": { 211 | "lastModified": 1681028828, 212 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 213 | "owner": "nix-systems", 214 | "repo": "default", 215 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 216 | "type": "github" 217 | }, 218 | "original": { 219 | "id": "systems", 220 | "type": "indirect" 221 | } 222 | }, 223 | "systems_3": { 224 | "locked": { 225 | "lastModified": 1681028828, 226 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 227 | "owner": "nix-systems", 228 | "repo": "default", 229 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 230 | "type": "github" 231 | }, 232 | "original": { 233 | "id": "systems", 234 | "type": "indirect" 235 | } 236 | }, 237 | "treefmt-nix": { 238 | "inputs": { 239 | "nixpkgs": [ 240 | "poetry2nix", 241 | "nixpkgs" 242 | ] 243 | }, 244 | "locked": { 245 | "lastModified": 1719749022, 246 | "narHash": "sha256-ddPKHcqaKCIFSFc/cvxS14goUhCOAwsM1PbMr0ZtHMg=", 247 | "owner": "numtide", 248 | "repo": "treefmt-nix", 249 | "rev": "8df5ff62195d4e67e2264df0b7f5e8c9995fd0bd", 250 | "type": "github" 251 | }, 252 | "original": { 253 | "owner": "numtide", 254 | "repo": "treefmt-nix", 255 | "type": "github" 256 | } 257 | } 258 | }, 259 | "root": "root", 260 | "version": 7 261 | } 262 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | poetry2nix = { 4 | url = "github:nix-community/poetry2nix"; 5 | inputs.nixpkgs.follows = "nixpkgs"; 6 | }; 7 | alejandra = { 8 | url = "github:kamadorueda/alejandra/3.0.0"; 9 | inputs.nixpkgs.follows = "nixpkgs"; 10 | }; 11 | }; 12 | outputs = inputs @ { 13 | self, 14 | nixpkgs, 15 | flake-parts, 16 | systems, 17 | poetry2nix, 18 | alejandra, 19 | ... 20 | }: 21 | flake-parts.lib.mkFlake {inherit inputs;} { 22 | systems = import systems; 23 | perSystem = { 24 | pkgs, 25 | lib, 26 | system, 27 | self', 28 | ... 29 | }: let 30 | poetryEnv = pkgs.callPackage ./. {}; 31 | in { 32 | _module.args.pkgs = import nixpkgs { 33 | inherit system; 34 | overlays = [poetry2nix.overlays.default]; 35 | }; 36 | 37 | devShells.default = pkgs.mkShell { 38 | packages = [ 39 | pkgs.poetry 40 | poetryEnv 41 | ]; 42 | POETRY_VIRTUALENVS_IN_PROJECT = true; 43 | shellHook = '' 44 | ${lib.getExe pkgs.poetry} env use ${lib.getExe pkgs.python3} 45 | ${lib.getExe pkgs.poetry} install --all-extras --no-root --sync 46 | 47 | pre-commit install --overwrite 48 | set -a 49 | source .env 2> /dev/null 50 | ''; 51 | }; 52 | }; 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "anyio" 5 | version = "4.9.0" 6 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 7 | optional = false 8 | python-versions = ">=3.9" 9 | files = [ 10 | {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, 11 | {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}, 12 | ] 13 | 14 | [package.dependencies] 15 | exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} 16 | idna = ">=2.8" 17 | sniffio = ">=1.1" 18 | typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} 19 | 20 | [package.extras] 21 | doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] 22 | test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] 23 | trio = ["trio (>=0.26.1)"] 24 | 25 | [[package]] 26 | name = "certifi" 27 | version = "2025.4.26" 28 | description = "Python package for providing Mozilla's CA Bundle." 29 | optional = false 30 | python-versions = ">=3.6" 31 | files = [ 32 | {file = "certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3"}, 33 | {file = "certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6"}, 34 | ] 35 | 36 | [[package]] 37 | name = "cfgv" 38 | version = "3.4.0" 39 | description = "Validate configuration and produce human readable error messages." 40 | optional = false 41 | python-versions = ">=3.8" 42 | files = [ 43 | {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, 44 | {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, 45 | ] 46 | 47 | [[package]] 48 | name = "colorama" 49 | version = "0.4.6" 50 | description = "Cross-platform colored terminal text." 51 | optional = false 52 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 53 | files = [ 54 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 55 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 56 | ] 57 | 58 | [[package]] 59 | name = "distlib" 60 | version = "0.3.9" 61 | description = "Distribution utilities" 62 | optional = false 63 | python-versions = "*" 64 | files = [ 65 | {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, 66 | {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, 67 | ] 68 | 69 | [[package]] 70 | name = "exceptiongroup" 71 | version = "1.2.2" 72 | description = "Backport of PEP 654 (exception groups)" 73 | optional = false 74 | python-versions = ">=3.7" 75 | files = [ 76 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, 77 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, 78 | ] 79 | 80 | [package.extras] 81 | test = ["pytest (>=6)"] 82 | 83 | [[package]] 84 | name = "filelock" 85 | version = "3.18.0" 86 | description = "A platform independent file lock." 87 | optional = false 88 | python-versions = ">=3.9" 89 | files = [ 90 | {file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"}, 91 | {file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"}, 92 | ] 93 | 94 | [package.extras] 95 | docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] 96 | testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] 97 | typing = ["typing-extensions (>=4.12.2)"] 98 | 99 | [[package]] 100 | name = "h11" 101 | version = "0.16.0" 102 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 103 | optional = false 104 | python-versions = ">=3.8" 105 | files = [ 106 | {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, 107 | {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, 108 | ] 109 | 110 | [[package]] 111 | name = "httpcore" 112 | version = "1.0.9" 113 | description = "A minimal low-level HTTP client." 114 | optional = false 115 | python-versions = ">=3.8" 116 | files = [ 117 | {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, 118 | {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, 119 | ] 120 | 121 | [package.dependencies] 122 | certifi = "*" 123 | h11 = ">=0.16" 124 | 125 | [package.extras] 126 | asyncio = ["anyio (>=4.0,<5.0)"] 127 | http2 = ["h2 (>=3,<5)"] 128 | socks = ["socksio (==1.*)"] 129 | trio = ["trio (>=0.22.0,<1.0)"] 130 | 131 | [[package]] 132 | name = "httpx" 133 | version = "0.27.2" 134 | description = "The next generation HTTP client." 135 | optional = false 136 | python-versions = ">=3.8" 137 | files = [ 138 | {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, 139 | {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, 140 | ] 141 | 142 | [package.dependencies] 143 | anyio = "*" 144 | certifi = "*" 145 | httpcore = "==1.*" 146 | idna = "*" 147 | sniffio = "*" 148 | 149 | [package.extras] 150 | brotli = ["brotli", "brotlicffi"] 151 | cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] 152 | http2 = ["h2 (>=3,<5)"] 153 | socks = ["socksio (==1.*)"] 154 | zstd = ["zstandard (>=0.18.0)"] 155 | 156 | [[package]] 157 | name = "identify" 158 | version = "2.6.10" 159 | description = "File identification library for Python" 160 | optional = false 161 | python-versions = ">=3.9" 162 | files = [ 163 | {file = "identify-2.6.10-py2.py3-none-any.whl", hash = "sha256:5f34248f54136beed1a7ba6a6b5c4b6cf21ff495aac7c359e1ef831ae3b8ab25"}, 164 | {file = "identify-2.6.10.tar.gz", hash = "sha256:45e92fd704f3da71cc3880036633f48b4b7265fd4de2b57627cb157216eb7eb8"}, 165 | ] 166 | 167 | [package.extras] 168 | license = ["ukkonen"] 169 | 170 | [[package]] 171 | name = "idna" 172 | version = "3.10" 173 | description = "Internationalized Domain Names in Applications (IDNA)" 174 | optional = false 175 | python-versions = ">=3.6" 176 | files = [ 177 | {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, 178 | {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, 179 | ] 180 | 181 | [package.extras] 182 | all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 183 | 184 | [[package]] 185 | name = "iniconfig" 186 | version = "2.1.0" 187 | description = "brain-dead simple config-ini parsing" 188 | optional = false 189 | python-versions = ">=3.8" 190 | files = [ 191 | {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, 192 | {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, 193 | ] 194 | 195 | [[package]] 196 | name = "mypy" 197 | version = "1.15.0" 198 | description = "Optional static typing for Python" 199 | optional = false 200 | python-versions = ">=3.9" 201 | files = [ 202 | {file = "mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13"}, 203 | {file = "mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559"}, 204 | {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b"}, 205 | {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3"}, 206 | {file = "mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b"}, 207 | {file = "mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828"}, 208 | {file = "mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f"}, 209 | {file = "mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5"}, 210 | {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e"}, 211 | {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c"}, 212 | {file = "mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f"}, 213 | {file = "mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f"}, 214 | {file = "mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd"}, 215 | {file = "mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f"}, 216 | {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464"}, 217 | {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee"}, 218 | {file = "mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e"}, 219 | {file = "mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22"}, 220 | {file = "mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445"}, 221 | {file = "mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d"}, 222 | {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5"}, 223 | {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036"}, 224 | {file = "mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357"}, 225 | {file = "mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf"}, 226 | {file = "mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078"}, 227 | {file = "mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba"}, 228 | {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5"}, 229 | {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b"}, 230 | {file = "mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2"}, 231 | {file = "mypy-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980"}, 232 | {file = "mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e"}, 233 | {file = "mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43"}, 234 | ] 235 | 236 | [package.dependencies] 237 | mypy_extensions = ">=1.0.0" 238 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 239 | typing_extensions = ">=4.6.0" 240 | 241 | [package.extras] 242 | dmypy = ["psutil (>=4.0)"] 243 | faster-cache = ["orjson"] 244 | install-types = ["pip"] 245 | mypyc = ["setuptools (>=50)"] 246 | reports = ["lxml"] 247 | 248 | [[package]] 249 | name = "mypy-extensions" 250 | version = "1.1.0" 251 | description = "Type system extensions for programs checked with the mypy type checker." 252 | optional = false 253 | python-versions = ">=3.8" 254 | files = [ 255 | {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, 256 | {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, 257 | ] 258 | 259 | [[package]] 260 | name = "nodeenv" 261 | version = "1.9.1" 262 | description = "Node.js virtual environment builder" 263 | optional = false 264 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 265 | files = [ 266 | {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, 267 | {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, 268 | ] 269 | 270 | [[package]] 271 | name = "packaging" 272 | version = "25.0" 273 | description = "Core utilities for Python packages" 274 | optional = false 275 | python-versions = ">=3.8" 276 | files = [ 277 | {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, 278 | {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, 279 | ] 280 | 281 | [[package]] 282 | name = "platformdirs" 283 | version = "4.3.7" 284 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 285 | optional = false 286 | python-versions = ">=3.9" 287 | files = [ 288 | {file = "platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94"}, 289 | {file = "platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351"}, 290 | ] 291 | 292 | [package.extras] 293 | docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] 294 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] 295 | type = ["mypy (>=1.14.1)"] 296 | 297 | [[package]] 298 | name = "pluggy" 299 | version = "1.5.0" 300 | description = "plugin and hook calling mechanisms for python" 301 | optional = false 302 | python-versions = ">=3.8" 303 | files = [ 304 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 305 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 306 | ] 307 | 308 | [package.extras] 309 | dev = ["pre-commit", "tox"] 310 | testing = ["pytest", "pytest-benchmark"] 311 | 312 | [[package]] 313 | name = "pre-commit" 314 | version = "3.8.0" 315 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 316 | optional = false 317 | python-versions = ">=3.9" 318 | files = [ 319 | {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, 320 | {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, 321 | ] 322 | 323 | [package.dependencies] 324 | cfgv = ">=2.0.0" 325 | identify = ">=1.0.0" 326 | nodeenv = ">=0.11.1" 327 | pyyaml = ">=5.1" 328 | virtualenv = ">=20.10.0" 329 | 330 | [[package]] 331 | name = "pytest" 332 | version = "8.3.5" 333 | description = "pytest: simple powerful testing with Python" 334 | optional = false 335 | python-versions = ">=3.8" 336 | files = [ 337 | {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, 338 | {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, 339 | ] 340 | 341 | [package.dependencies] 342 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 343 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 344 | iniconfig = "*" 345 | packaging = "*" 346 | pluggy = ">=1.5,<2" 347 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 348 | 349 | [package.extras] 350 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 351 | 352 | [[package]] 353 | name = "pyyaml" 354 | version = "6.0.2" 355 | description = "YAML parser and emitter for Python" 356 | optional = false 357 | python-versions = ">=3.8" 358 | files = [ 359 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, 360 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, 361 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, 362 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, 363 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, 364 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, 365 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, 366 | {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, 367 | {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, 368 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, 369 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, 370 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, 371 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, 372 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, 373 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, 374 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, 375 | {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, 376 | {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, 377 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, 378 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, 379 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, 380 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, 381 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, 382 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, 383 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, 384 | {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, 385 | {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, 386 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, 387 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, 388 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, 389 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, 390 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, 391 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, 392 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, 393 | {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, 394 | {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, 395 | {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, 396 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, 397 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, 398 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, 399 | {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, 400 | {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, 401 | {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, 402 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, 403 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, 404 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, 405 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, 406 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, 407 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, 408 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, 409 | {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, 410 | {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, 411 | {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, 412 | ] 413 | 414 | [[package]] 415 | name = "respx" 416 | version = "0.21.1" 417 | description = "A utility for mocking out the Python HTTPX and HTTP Core libraries." 418 | optional = false 419 | python-versions = ">=3.7" 420 | files = [ 421 | {file = "respx-0.21.1-py2.py3-none-any.whl", hash = "sha256:05f45de23f0c785862a2c92a3e173916e8ca88e4caad715dd5f68584d6053c20"}, 422 | {file = "respx-0.21.1.tar.gz", hash = "sha256:0bd7fe21bfaa52106caa1223ce61224cf30786985f17c63c5d71eff0307ee8af"}, 423 | ] 424 | 425 | [package.dependencies] 426 | httpx = ">=0.21.0" 427 | 428 | [[package]] 429 | name = "ruff" 430 | version = "0.5.7" 431 | description = "An extremely fast Python linter and code formatter, written in Rust." 432 | optional = false 433 | python-versions = ">=3.7" 434 | files = [ 435 | {file = "ruff-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a"}, 436 | {file = "ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be"}, 437 | {file = "ruff-0.5.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf3d86a1fdac1aec8a3417a63587d93f906c678bb9ed0b796da7b59c1114a1e"}, 438 | {file = "ruff-0.5.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a01c34400097b06cf8a6e61b35d6d456d5bd1ae6961542de18ec81eaf33b4cb8"}, 439 | {file = "ruff-0.5.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcc8054f1a717e2213500edaddcf1dbb0abad40d98e1bd9d0ad364f75c763eea"}, 440 | {file = "ruff-0.5.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f70284e73f36558ef51602254451e50dd6cc479f8b6f8413a95fcb5db4a55fc"}, 441 | {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a78ad870ae3c460394fc95437d43deb5c04b5c29297815a2a1de028903f19692"}, 442 | {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ccd078c66a8e419475174bfe60a69adb36ce04f8d4e91b006f1329d5cd44bcf"}, 443 | {file = "ruff-0.5.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e31c9bad4ebf8fdb77b59cae75814440731060a09a0e0077d559a556453acbb"}, 444 | {file = "ruff-0.5.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d796327eed8e168164346b769dd9a27a70e0298d667b4ecee6877ce8095ec8e"}, 445 | {file = "ruff-0.5.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a09ea2c3f7778cc635e7f6edf57d566a8ee8f485f3c4454db7771efb692c499"}, 446 | {file = "ruff-0.5.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a36d8dcf55b3a3bc353270d544fb170d75d2dff41eba5df57b4e0b67a95bb64e"}, 447 | {file = "ruff-0.5.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9369c218f789eefbd1b8d82a8cf25017b523ac47d96b2f531eba73770971c9e5"}, 448 | {file = "ruff-0.5.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b88ca3db7eb377eb24fb7c82840546fb7acef75af4a74bd36e9ceb37a890257e"}, 449 | {file = "ruff-0.5.7-py3-none-win32.whl", hash = "sha256:33d61fc0e902198a3e55719f4be6b375b28f860b09c281e4bdbf783c0566576a"}, 450 | {file = "ruff-0.5.7-py3-none-win_amd64.whl", hash = "sha256:083bbcbe6fadb93cd86709037acc510f86eed5a314203079df174c40bbbca6b3"}, 451 | {file = "ruff-0.5.7-py3-none-win_arm64.whl", hash = "sha256:2dca26154ff9571995107221d0aeaad0e75a77b5a682d6236cf89a58c70b76f4"}, 452 | {file = "ruff-0.5.7.tar.gz", hash = "sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5"}, 453 | ] 454 | 455 | [[package]] 456 | name = "sniffio" 457 | version = "1.3.1" 458 | description = "Sniff out which async library your code is running under" 459 | optional = false 460 | python-versions = ">=3.7" 461 | files = [ 462 | {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, 463 | {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, 464 | ] 465 | 466 | [[package]] 467 | name = "tomli" 468 | version = "2.2.1" 469 | description = "A lil' TOML parser" 470 | optional = false 471 | python-versions = ">=3.8" 472 | files = [ 473 | {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, 474 | {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, 475 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, 476 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, 477 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, 478 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, 479 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, 480 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, 481 | {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, 482 | {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, 483 | {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, 484 | {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, 485 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, 486 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, 487 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, 488 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, 489 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, 490 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, 491 | {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, 492 | {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, 493 | {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, 494 | {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, 495 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, 496 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, 497 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, 498 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, 499 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, 500 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, 501 | {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, 502 | {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, 503 | {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, 504 | {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, 505 | ] 506 | 507 | [[package]] 508 | name = "typing-extensions" 509 | version = "4.13.2" 510 | description = "Backported and Experimental Type Hints for Python 3.8+" 511 | optional = false 512 | python-versions = ">=3.8" 513 | files = [ 514 | {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, 515 | {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, 516 | ] 517 | 518 | [[package]] 519 | name = "virtualenv" 520 | version = "20.30.0" 521 | description = "Virtual Python Environment builder" 522 | optional = false 523 | python-versions = ">=3.8" 524 | files = [ 525 | {file = "virtualenv-20.30.0-py3-none-any.whl", hash = "sha256:e34302959180fca3af42d1800df014b35019490b119eba981af27f2fa486e5d6"}, 526 | {file = "virtualenv-20.30.0.tar.gz", hash = "sha256:800863162bcaa5450a6e4d721049730e7f2dae07720e0902b0e4040bd6f9ada8"}, 527 | ] 528 | 529 | [package.dependencies] 530 | distlib = ">=0.3.7,<1" 531 | filelock = ">=3.12.2,<4" 532 | platformdirs = ">=3.9.1,<5" 533 | 534 | [package.extras] 535 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 536 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] 537 | 538 | [metadata] 539 | lock-version = "2.0" 540 | python-versions = "^3.10" 541 | content-hash = "60c58300bd9229b8377da51bb9409492688fab1db85b5b39c1eb80ca39269833" 542 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | authors = ["dunderrrrrr "] 3 | description = "A python API wrapper for blocket.se" 4 | name = "blocket_api" 5 | readme = "README.md" 6 | version = "0.2.0" 7 | 8 | [tool.poetry.dependencies] 9 | httpx = "^0.27.0" 10 | python = "^3.10" 11 | pre-commit = "^3.7.1" 12 | ruff = "^0.5.2" 13 | pytest = "^8.2.2" 14 | respx = "^0.21.1" 15 | mypy = "^1.15.0" 16 | 17 | [project.urls] 18 | Homepage = "https://github.com/dunderrrrrr/blocket_api" 19 | Repository = "https://github.com/dunderrrrrr/blocket_api" 20 | Documentation = "https://github.com/dunderrrrrr/blocket_api/blob/main/README.md" 21 | Issues = "https://github.com/dunderrrrrr/blocket_api/issues" 22 | 23 | [build-system] 24 | build-backend = "poetry.core.masonry.api" 25 | requires = ["poetry-core"] 26 | 27 | 28 | [tool.pytest.ini_options] 29 | testpaths = [ 30 | "tests/*", 31 | ] 32 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dunderrrrrr/blocket_api/0d995a0d9e8e8ebc8ef11e464cc9e977513777ab/tests/__init__.py -------------------------------------------------------------------------------- /tests/assertions.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from blocket_api.blocket import BlocketAPI, LimitError 3 | 4 | api = BlocketAPI("token") 5 | 6 | 7 | def test_limit_errors() -> None: 8 | with pytest.raises(LimitError): 9 | api.get_listings(limit=100) 10 | 11 | with pytest.raises(LimitError): 12 | api.custom_search("saab", limit=100) 13 | 14 | 15 | def test_typeerrors() -> None: 16 | with pytest.raises(TypeError): 17 | # missing search query 18 | api.custom_search() # type: ignore[call-arg] 19 | -------------------------------------------------------------------------------- /tests/requests.py: -------------------------------------------------------------------------------- 1 | import respx 2 | import pytest 3 | from httpx import Response 4 | from blocket_api.blocket import BASE_URL, APIError, BlocketAPI, _make_request 5 | 6 | api = BlocketAPI("token") 7 | 8 | 9 | def test_make_request_no_raise() -> None: 10 | _make_request(url=f"{BASE_URL}/not_found", token="token", raise_for_status=False) 11 | 12 | 13 | @respx.mock 14 | def test_make_request_raise_404() -> None: 15 | respx.get(f"{BASE_URL}/not_found").mock( 16 | return_value=Response(status_code=404), 17 | ) 18 | with pytest.raises(APIError): 19 | _make_request(url=f"{BASE_URL}/not_found", token="token", raise_for_status=True) 20 | 21 | 22 | @respx.mock 23 | def test_make_request_raise_401() -> None: 24 | respx.get(f"{BASE_URL}/unauthorized").mock( 25 | return_value=Response(status_code=401), 26 | ) 27 | with pytest.raises(APIError): 28 | _make_request(url=f"{BASE_URL}/unauthorized", token="token") 29 | -------------------------------------------------------------------------------- /tests/searches.py: -------------------------------------------------------------------------------- 1 | import respx 2 | from httpx import Response 3 | from blocket_api.blocket import BASE_URL, BlocketAPI, Region 4 | 5 | api = BlocketAPI("token") 6 | 7 | 8 | @respx.mock 9 | def test_saved_searches() -> None: 10 | """ 11 | Make sure mobility saved searches are merged with v2/searches. 12 | """ 13 | respx.get(f"{BASE_URL}/saved/v2/searches").mock( 14 | return_value=Response( 15 | status_code=200, 16 | json={ 17 | "data": [ 18 | {"id": "1", "name": '"buggy", Bilar säljes i hela Sverige'}, 19 | {"id": "2", "name": "Cyklar säljes i flera kommuner"}, 20 | ], 21 | }, 22 | ), 23 | ) 24 | respx.get(f"{BASE_URL}/mobility-saved-searches/v1/searches").mock( 25 | return_value=Response( 26 | status_code=200, 27 | json={"data": [{"id": "3", "name": "Bilar säljes i hela Sverige"}]}, 28 | ), 29 | ) 30 | assert api.saved_searches() == [ 31 | {"id": "1", "name": '"buggy", Bilar säljes i hela Sverige'}, 32 | {"id": "2", "name": "Cyklar säljes i flera kommuner"}, 33 | {"id": "3", "name": "Bilar säljes i hela Sverige"}, 34 | ] 35 | 36 | 37 | @respx.mock 38 | def test_for_search_id() -> None: 39 | respx.get(f"{BASE_URL}/saved/v2/searches_content/123?lim=99").mock( 40 | return_value=Response(status_code=200, json={"data": "listings-data"}), 41 | ) 42 | assert api.get_listings(search_id=123) == {"data": "listings-data"} 43 | 44 | 45 | @respx.mock 46 | def test_for_search_id_mobility() -> None: 47 | respx.get(f"{BASE_URL}/saved/v2/searches_content/123?lim=99").mock( 48 | return_value=Response(status_code=404), 49 | ) 50 | respx.get(f"{BASE_URL}/mobility-saved-searches/v1/searches/123/ads?lim=99").mock( 51 | return_value=Response(status_code=200, json={"data": "mobility-data"}), 52 | ) 53 | assert api.get_listings(search_id=123) == {"data": "mobility-data"} 54 | 55 | 56 | @respx.mock 57 | def test_custom_search() -> None: 58 | respx.get( 59 | f"{BASE_URL}/search_bff/v2/content?lim=99&q=saab&r=20&status=active" 60 | ).mock( 61 | return_value=Response(status_code=200, json={"data": {"location": "halland"}}), 62 | ) 63 | assert api.custom_search("saab", Region.halland) == { 64 | "data": {"location": "halland"} 65 | } 66 | 67 | 68 | class Test_MotorSearchURLs: 69 | @respx.mock 70 | def test_make_filter(self) -> None: 71 | expected_url_filter = '?filter={"key": "make", "values": ["Audi", "Toyota"]}' 72 | respx.get( 73 | f"{BASE_URL}/motor-search-service/v4/search/car" 74 | f"{expected_url_filter}" 75 | "&page=1" 76 | ).mock( 77 | return_value=Response(status_code=200, json={"data": "ok"}), 78 | ) 79 | assert api.motor_search(page=1, make=["Audi", "Toyota"]) == {"data": "ok"} 80 | 81 | @respx.mock 82 | def test_range_filters(self) -> None: 83 | expected_url_filter = ( 84 | '?filter={"key": "make", "values": ["Ford"]}' 85 | '&filter={"key": "price", "range": {"start": "1000", "end": "2000"}}' 86 | '&filter={"key": "modelYear", "range": {"start": "1995", "end": "2000"}}' 87 | '&filter={"key": "milage", "range": {"start": "1000", "end": "5000"}}' 88 | '&filter={"key": "gearbox", "values": "Manuell"}' 89 | ) 90 | respx.get( 91 | f"{BASE_URL}/motor-search-service/v4/search/car" 92 | f"{expected_url_filter}" 93 | "&page=5" 94 | ).mock( 95 | return_value=Response(status_code=200, json={"data": "ok"}), 96 | ) 97 | assert api.motor_search( 98 | page=5, 99 | make=["Ford"], 100 | price=(1000, 2000), 101 | modelYear=(1995, 2000), 102 | milage=(1000, 5000), 103 | gearbox="Manuell", 104 | ) == {"data": "ok"} 105 | --------------------------------------------------------------------------------