From caa0bd9901f3fb4f30fcad0c9785c7aa3883b6ac Mon Sep 17 00:00:00 2001 From: Emil Date: Thu, 18 Jul 2024 13:22:41 +0200 Subject: [PATCH 1/4] Limit exception --- blocket_api/blocket.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/blocket_api/blocket.py b/blocket_api/blocket.py index 49dcc19..33120d1 100644 --- a/blocket_api/blocket.py +++ b/blocket_api/blocket.py @@ -38,8 +38,10 @@ class Region(Enum): BASE_URL = "https://api.blocket.se" -class APIError(Exception): - ... +class APIError(Exception): ... + + +class LimitError(Exception): ... def _make_request(*, url, token: str, raise_for_status: bool = True) -> Response: @@ -101,7 +103,7 @@ def get_listings(self, search_id: int | None = None, limit: int = 99) -> dict: Retrieve listings/ads based on the provided search criteria. """ if limit > 99: - raise AssertionError("Limit cannot be greater than 99.") + raise LimitError("Limit cannot be greater than 99.") if search_id: return self._for_search_id(search_id, limit) @@ -119,10 +121,7 @@ def custom_search( Supply a region for filtering. Default is all of Sweden. """ if limit > 99: - raise AssertionError("Limit cannot be greater than 99.") - - if not search_query: - raise AssertionError("A search query is required.") + raise LimitError("Limit cannot be greater than 99.") return _make_request( url=f"{BASE_URL}/search_bff/v2/content?lim={limit}&q={search_query}&r={region.value}&status=active", From 4ea5f4581a04364c16d837837876c96859f43552 Mon Sep 17 00:00:00 2001 From: Emil Date: Thu, 18 Jul 2024 13:23:16 +0200 Subject: [PATCH 2/4] Add pytest and respx --- poetry.lock | 109 +++++++++++++++++++++++++++++++++++++++++++++---- pyproject.toml | 8 ++++ 2 files changed, 110 insertions(+), 7 deletions(-) diff --git a/poetry.lock b/poetry.lock index 6729912..afd66ad 100644 --- a/poetry.lock +++ b/poetry.lock @@ -44,6 +44,17 @@ files = [ {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, ] +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + [[package]] name = "distlib" version = "0.3.8" @@ -71,19 +82,19 @@ test = ["pytest (>=6)"] [[package]] name = "filelock" -version = "3.15.4" +version = "3.12.4" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ - {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, - {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, + {file = "filelock-3.12.4-py3-none-any.whl", hash = "sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4"}, + {file = "filelock-3.12.4.tar.gz", hash = "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd"}, ] [package.extras] -docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] -typing = ["typing-extensions (>=4.8)"] +docs = ["furo (>=2023.7.26)", "sphinx (>=7.1.2)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3)", "diff-cover (>=7.7)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-timeout (>=2.1)"] +typing = ["typing-extensions (>=4.7.1)"] [[package]] name = "h11" @@ -166,6 +177,17 @@ files = [ {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + [[package]] name = "nodeenv" version = "1.9.1" @@ -177,6 +199,17 @@ files = [ {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, ] +[[package]] +name = "packaging" +version = "24.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] + [[package]] name = "platformdirs" version = "4.2.2" @@ -193,6 +226,21 @@ docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx- test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] type = ["mypy (>=1.8)"] +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + [[package]] name = "pre-commit" version = "3.7.1" @@ -211,6 +259,28 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" +[[package]] +name = "pytest" +version = "8.2.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, + {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2.0" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + [[package]] name = "pyyaml" version = "6.0.1" @@ -271,6 +341,20 @@ files = [ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] +[[package]] +name = "respx" +version = "0.21.1" +description = "A utility for mocking out the Python HTTPX and HTTP Core libraries." +optional = false +python-versions = ">=3.7" +files = [ + {file = "respx-0.21.1-py2.py3-none-any.whl", hash = "sha256:05f45de23f0c785862a2c92a3e173916e8ca88e4caad715dd5f68584d6053c20"}, + {file = "respx-0.21.1.tar.gz", hash = "sha256:0bd7fe21bfaa52106caa1223ce61224cf30786985f17c63c5d71eff0307ee8af"}, +] + +[package.dependencies] +httpx = ">=0.21.0" + [[package]] name = "ruff" version = "0.5.2" @@ -309,6 +393,17 @@ files = [ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + [[package]] name = "typing-extensions" version = "4.12.2" @@ -343,4 +438,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "bf22f74291a7a3f5cc12cfa3c8770999b143f30116659d33a39bf0b8889fcfbc" +content-hash = "6d262a6cd338628b473ff708b847c41d5783f207b5d448c3b3c0ccc6b1e5a2d6" diff --git a/pyproject.toml b/pyproject.toml index b05634e..b4b5789 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,8 @@ httpx = "^0.27.0" python = "^3.10" pre-commit = "^3.7.1" ruff = "^0.5.2" +pytest = "^8.2.2" +respx = "^0.21.1" [project.urls] Homepage = "https://github.com/dunderrrrrr/blocket_api" @@ -20,3 +22,9 @@ Issues = "https://github.com/dunderrrrrr/blocket_api/issues" [build-system] build-backend = "poetry.core.masonry.api" requires = ["poetry-core"] + + +[tool.pytest.ini_options] +testpaths = [ + "tests/*", +] From 606c548da7f89506b39f8666ad6fa1b6a8daf828 Mon Sep 17 00:00:00 2001 From: Emil Date: Thu, 18 Jul 2024 13:23:37 +0200 Subject: [PATCH 3/4] Add some tests --- tests/__init__.py | 0 tests/assertions.py | 20 ++++++++++++++ tests/requests.py | 28 ++++++++++++++++++++ tests/searches.py | 63 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 111 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/assertions.py create mode 100644 tests/requests.py create mode 100644 tests/searches.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/assertions.py b/tests/assertions.py new file mode 100644 index 0000000..0ba4fc5 --- /dev/null +++ b/tests/assertions.py @@ -0,0 +1,20 @@ +import pytest +from blocket_api.blocket import BlocketAPI, LimitError + +api = BlocketAPI("token") + + +def test_limit_errors(): + with pytest.raises(LimitError): + api.get_listings(limit=100) + + with pytest.raises(LimitError): + api.custom_search("saab", limit=100) + + +def test_typeerrors(): + with pytest.raises(TypeError): + api.custom_search() # missing search query + + with pytest.raises(TypeError): + BlocketAPI() diff --git a/tests/requests.py b/tests/requests.py new file mode 100644 index 0000000..50eff9d --- /dev/null +++ b/tests/requests.py @@ -0,0 +1,28 @@ +import respx +import pytest +from httpx import Response +from blocket_api.blocket import BASE_URL, APIError, BlocketAPI, _make_request + +api = BlocketAPI("token") + + +def test_make_request_no_raise(): + _make_request(url=f"{BASE_URL}/not_found", token="token", raise_for_status=False) + + +@respx.mock +def test_make_request_raise_404(): + respx.get(f"{BASE_URL}/not_found").mock( + return_value=Response(status_code=404), + ) + with pytest.raises(APIError): + _make_request(url=f"{BASE_URL}/not_found", token="token", raise_for_status=True) + + +@respx.mock +def test_make_request_raise_401(): + respx.get(f"{BASE_URL}/unauthorized").mock( + return_value=Response(status_code=401), + ) + with pytest.raises(APIError): + _make_request(url=f"{BASE_URL}/unauthorized", token="token") diff --git a/tests/searches.py b/tests/searches.py new file mode 100644 index 0000000..bf37f66 --- /dev/null +++ b/tests/searches.py @@ -0,0 +1,63 @@ +import respx +from httpx import Response +from blocket_api.blocket import BASE_URL, BlocketAPI, Region + +api = BlocketAPI("token") + + +@respx.mock +def test_saved_searches(): + """ + Make sure mobility saved searches are merged with v2/searches. + """ + respx.get(f"{BASE_URL}/saved/v2/searches").mock( + return_value=Response( + status_code=200, + json={ + "data": [ + {"id": "1", "name": '"buggy", Bilar säljes i hela Sverige'}, + {"id": "2", "name": "Cyklar säljes i flera kommuner"}, + ], + }, + ), + ) + respx.get(f"{BASE_URL}/mobility-saved-searches/v1/searches").mock( + return_value=Response( + status_code=200, + json={"data": [{"id": "3", "name": "Bilar säljes i hela Sverige"}]}, + ), + ) + assert api.saved_searches() == [ + {"id": "1", "name": '"buggy", Bilar säljes i hela Sverige'}, + {"id": "2", "name": "Cyklar säljes i flera kommuner"}, + {"id": "3", "name": "Bilar säljes i hela Sverige"}, + ] + + +@respx.mock +def test_for_search_id(): + respx.get(f"{BASE_URL}/saved/v2/searches_content/123?lim=99").mock( + return_value=Response(status_code=200, json={"data": "listings-data"}), + ) + assert api.get_listings(search_id=123) == {"data": "listings-data"} + + +@respx.mock +def test_for_search_id_mobility(): + respx.get(f"{BASE_URL}/saved/v2/searches_content/123?lim=99").mock( + return_value=Response(status_code=404), + ) + respx.get(f"{BASE_URL}/mobility-saved-searches/v1/searches/123/ads?lim=99").mock( + return_value=Response(status_code=200, json={"data": "mobility-data"}), + ) + assert api.get_listings(search_id=123) == {"data": "mobility-data"} + + +@respx.mock +def test_custom_search(): + respx.get( + f"{BASE_URL}/search_bff/v2/content?lim=99&q=saab&r=20&status=active" + ).mock( + return_value=Response(status_code=200, json={"data": {"location": "halland"}}), + ) + api.custom_search("saab", Region.halland) From 714fa0a217a0a3fe21ce23b3e5480cfebbdd053b Mon Sep 17 00:00:00 2001 From: Emil Date: Thu, 18 Jul 2024 13:23:54 +0200 Subject: [PATCH 4/4] Test workflow --- .github/workflows/tests.yml | 46 +++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..6f43c69 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,46 @@ +name: Push +on: [push] + +jobs: + pytest: + strategy: + fail-fast: false + matrix: + python-version: [3.12] + poetry-version: [1.8.3] + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Run image + uses: abatilo/actions-poetry@v2.0.0 + with: + poetry-version: ${{ matrix.poetry-version }} + - name: Install dependencies + run: poetry install + - name: Run tests + run: poetry run pytest + ruff: + strategy: + fail-fast: false + matrix: + python-version: [3.12] + poetry-version: [1.8.3] + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Run image + uses: abatilo/actions-poetry@v2.0.0 + with: + poetry-version: ${{ matrix.poetry-version }} + - name: Install dependencies + run: poetry install + - name: Check ruff + run: poetry run ruff check .