From 66ea1c9f87e42a43496fe3a1928f4ea74ce63ced Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 16 Nov 2023 00:10:37 +0100 Subject: [PATCH 1/2] Switch from requests to httpx And from respones to pytest-httpx. --- pyproject.toml | 4 +- src/brreg/_exceptions.py | 4 +- src/brreg/enhetsregisteret/_client.py | 11 +-- tests/test_enhetsregisteret.py | 97 +++++++++++++++------------ 4 files changed, 65 insertions(+), 51 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 34df524..11b22fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ classifiers = [ [tool.poetry.dependencies] python = ">= 3.8.0" attrs = ">= 22.2" -requests = ">= 2.28.1" +httpx = ">= 0.24" [tool.poetry.group.dev.dependencies] nox = "^2023.4.22" @@ -41,7 +41,7 @@ ruff = "^0.1.4" coverage = { extras = ["toml"], version = "^7.3.2" } pytest = "^7.4.2" pytest-cov = "^4.1.0" -responses = "^0.24.0" +pytest-httpx = "0.22.0" [tool.poetry.group.typing.dependencies] types-requests = "^2.31.0.10" diff --git a/src/brreg/_exceptions.py b/src/brreg/_exceptions.py index a63d062..1e191e4 100644 --- a/src/brreg/_exceptions.py +++ b/src/brreg/_exceptions.py @@ -18,9 +18,9 @@ def __init__( *, method: Optional[str], url: Optional[str], - status: Optional[int], + status_code: Optional[int], ) -> None: super().__init__(f"REST API exception: {msg}") self.method = method self.url = url - self.status = status + self.status_code = status_code diff --git a/src/brreg/enhetsregisteret/_client.py b/src/brreg/enhetsregisteret/_client.py index fba8714..252f763 100644 --- a/src/brreg/enhetsregisteret/_client.py +++ b/src/brreg/enhetsregisteret/_client.py @@ -1,6 +1,6 @@ from typing import Optional -import requests +import httpx from brreg import BrregError, BrregRestError @@ -18,8 +18,9 @@ def get_enhet(organisasjonsnummer: str) -> Optional[Enhet]: Raises :class:`BrregRestException` if a REST exception occures Raises :class:`BrregException` if an unhandled exception occures """ + res: Optional[httpx.Response] = None try: - res = requests.get(f"{BASE_URL}/enheter/{organisasjonsnummer}") + res = httpx.get(f"{BASE_URL}/enheter/{organisasjonsnummer}") if res.status_code in (404, 410): return None @@ -27,12 +28,12 @@ def get_enhet(organisasjonsnummer: str) -> Optional[Enhet]: res.raise_for_status() return Enhet.from_json(res.json()) - except requests.RequestException as exc: + except httpx.HTTPError as exc: raise BrregRestError( str(exc), method=(exc.request.method if exc.request else None), - url=(exc.request.url if exc.request else None), - status=getattr(exc.response, "status_code", None), + url=(str(exc.request.url) if exc.request else None), + status_code=(res.status_code if res else None), ) from exc except Exception as exc: raise BrregError(exc) from exc diff --git a/tests/test_enhetsregisteret.py b/tests/test_enhetsregisteret.py index c841465..0bf1455 100644 --- a/tests/test_enhetsregisteret.py +++ b/tests/test_enhetsregisteret.py @@ -1,19 +1,22 @@ from datetime import date +import httpx import pytest -import responses +from pytest_httpx import HTTPXMock from brreg import BrregRestError, enhetsregisteret -@responses.activate -def test_get_enhet(organization_details_response: bytes) -> None: - responses.add( - responses.GET, - "https://data.brreg.no/enhetsregisteret/api/enheter/818511752", - body=organization_details_response, - status=200, - content_type="application/json", +def test_get_enhet( + httpx_mock: HTTPXMock, + organization_details_response: bytes, +) -> None: + httpx_mock.add_response( # pyright: ignore[reportUnknownMemberType] + method="GET", + url="https://data.brreg.no/enhetsregisteret/api/enheter/818511752", + status_code=200, + headers={"content-type": "application/json"}, + content=organization_details_response, ) org = enhetsregisteret.get_enhet("818511752") @@ -50,14 +53,16 @@ def test_get_enhet(organization_details_response: bytes) -> None: assert org.slettedato is None -@responses.activate -def test_get_enhet_when_deleted(deleted_organization_details_response: bytes) -> None: - responses.add( - responses.GET, - "https://data.brreg.no/enhetsregisteret/api/enheter/815597222", - body=deleted_organization_details_response, - status=200, - content_type="application/json", +def test_get_enhet_when_deleted( + httpx_mock: HTTPXMock, + deleted_organization_details_response: bytes, +) -> None: + httpx_mock.add_response( # pyright: ignore[reportUnknownMemberType] + method="GET", + url="https://data.brreg.no/enhetsregisteret/api/enheter/815597222", + status_code=200, + headers={"content-type": "application/json"}, + content=deleted_organization_details_response, ) org = enhetsregisteret.get_enhet("815597222") @@ -71,13 +76,14 @@ def test_get_enhet_when_deleted(deleted_organization_details_response: bytes) -> assert org.slettedato == date(2017, 10, 20) -@responses.activate -def test_get_enhet_when_gone() -> None: - responses.add( - responses.GET, - "https://data.brreg.no/enhetsregisteret/api/enheter/818511752", - status=410, - content_type="application/json", +def test_get_enhet_when_gone( + httpx_mock: HTTPXMock, +) -> None: + httpx_mock.add_response( # pyright: ignore[reportUnknownMemberType] + method="GET", + url="https://data.brreg.no/enhetsregisteret/api/enheter/818511752", + status_code=410, + headers={"content-type": "application/json"}, ) org = enhetsregisteret.get_enhet("818511752") @@ -85,13 +91,14 @@ def test_get_enhet_when_gone() -> None: assert org is None -@responses.activate -def test_get_enhet_when_not_found() -> None: - responses.add( - responses.GET, - "https://data.brreg.no/enhetsregisteret/api/enheter/818511752", - status=404, - content_type="application/json", +def test_get_enhet_when_not_found( + httpx_mock: HTTPXMock, +) -> None: + httpx_mock.add_response( # pyright: ignore[reportUnknownMemberType] + method="GET", + url="https://data.brreg.no/enhetsregisteret/api/enheter/818511752", + status_code=404, + headers={"content-type": "application/json"}, ) org = enhetsregisteret.get_enhet("818511752") @@ -99,13 +106,14 @@ def test_get_enhet_when_not_found() -> None: assert org is None -@responses.activate -def test_get_enhet_when_http_error() -> None: - responses.add( - responses.GET, - "https://data.brreg.no/enhetsregisteret/api/enheter/818511752", - status=400, - content_type="application/json", +def test_get_enhet_when_http_error( + httpx_mock: HTTPXMock, +) -> None: + httpx_mock.add_response( # pyright: ignore[reportUnknownMemberType] + method="GET", + url="https://data.brreg.no/enhetsregisteret/api/enheter/818511752", + status_code=400, + headers={"content-type": "application/json"}, ) with pytest.raises(BrregRestError) as exc_info: @@ -119,11 +127,16 @@ def test_get_enhet_when_http_error() -> None: exc_info.value.url == "https://data.brreg.no/enhetsregisteret/api/enheter/818511752" ) - assert exc_info.value.status == 400 + assert exc_info.value.status_code == 400 -@responses.activate -def test_get_organization_by_number_when_http_timeout() -> None: +def test_get_organization_by_number_when_http_timeout( + httpx_mock: HTTPXMock, +) -> None: + httpx_mock.add_exception( # pyright: ignore[reportUnknownMemberType] + httpx.ConnectTimeout("Connection refused"), + ) + with pytest.raises(BrregRestError) as exc_info: enhetsregisteret.get_enhet("818511752") @@ -135,4 +148,4 @@ def test_get_organization_by_number_when_http_timeout() -> None: exc_info.value.url == "https://data.brreg.no/enhetsregisteret/api/enheter/818511752" ) - assert exc_info.value.status is None + assert exc_info.value.status_code is None From d010da9b17debda65b3a215dc7a831aaa16b4910 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 16 Nov 2023 00:30:56 +0100 Subject: [PATCH 2/2] Reuse HTTP connections across requests --- docs/api.rst | 8 ++- docs/quickstart.rst | 5 +- src/brreg/enhetsregisteret/__init__.py | 4 +- src/brreg/enhetsregisteret/_client.py | 78 +++++++++++++++++--------- tests/test_enhetsregisteret.py | 12 ++-- 5 files changed, 68 insertions(+), 39 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 0b6de71..464f786 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -7,12 +7,12 @@ API reference Exceptions ========== -.. autoclass:: BrregException +.. autoclass:: BrregError :members: :undoc-members: -.. autoclass:: BrregRestException +.. autoclass:: BrregRestError :members: :undoc-members: @@ -22,7 +22,9 @@ Enhetsregisteret .. automodule:: brreg.enhetsregisteret -.. autofunction:: brreg.enhetsregisteret.get_enhet +.. autoclass:: brreg.enhetsregisteret.Client + :members: + :undoc-members: .. autoclass:: brreg.enhetsregisteret.Enhet :members: diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 54a7b57..03c669f 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -11,8 +11,9 @@ Organization details by organization number To get details about an organization ("enhet") given its organization number: ->>> from brreg import enhetsregisteret ->>> enhet = enhetsregisteret.get_enhet('915501680') +>>> from brreg.enhetsregisteret import Client +>>> client = Client() +>>> enhet = client.get_enhet('915501680') >>> enhet.organisasjonsnummer '915501680' >>> enhet.navn diff --git a/src/brreg/enhetsregisteret/__init__.py b/src/brreg/enhetsregisteret/__init__.py index ea17f7c..7148bcc 100644 --- a/src/brreg/enhetsregisteret/__init__.py +++ b/src/brreg/enhetsregisteret/__init__.py @@ -3,7 +3,7 @@ See https://data.brreg.no/enhetsregisteret/api/docs/index.html for API details. """ -from brreg.enhetsregisteret._client import get_enhet +from brreg.enhetsregisteret._client import Client from brreg.enhetsregisteret._types import ( Adresse, Enhet, @@ -14,7 +14,7 @@ __all__ = [ # From _client module: - "get_enhet", + "Client", # From _types module: "Adresse", "Enhet", diff --git a/src/brreg/enhetsregisteret/_client.py b/src/brreg/enhetsregisteret/_client.py index 252f763..39f157d 100644 --- a/src/brreg/enhetsregisteret/_client.py +++ b/src/brreg/enhetsregisteret/_client.py @@ -1,39 +1,65 @@ -from typing import Optional +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional import httpx from brreg import BrregError, BrregRestError +from brreg.enhetsregisteret._types import Enhet + +if TYPE_CHECKING: + from types import TracebackType + -from ._types import Enhet +class Client: + client: httpx.Client -BASE_URL = "https://data.brreg.no/enhetsregisteret/api" + def __new__(cls) -> Client: + self = super().__new__(cls) + self.open() + return self + def __enter__(self) -> Client: + return self -def get_enhet(organisasjonsnummer: str) -> Optional[Enhet]: - """Get :class:`Enhet` given an organization number. + def __exit__( + self, + exc_type: type[BaseException] | None = None, + exc_value: BaseException | None = None, + traceback: TracebackType | None = None, + ) -> None: + self.close() - Returns :class:`None` if Enhet is gone or not found - Returns :class:`Enhet` if Enhet is found + def open(self) -> None: + self.client = httpx.Client( + base_url="https://data.brreg.no/enhetsregisteret/api", + ) - Raises :class:`BrregRestException` if a REST exception occures - Raises :class:`BrregException` if an unhandled exception occures - """ - res: Optional[httpx.Response] = None - try: - res = httpx.get(f"{BASE_URL}/enheter/{organisasjonsnummer}") + def close(self) -> None: + self.client.close() - if res.status_code in (404, 410): - return None + def get_enhet(self, organisasjonsnummer: str) -> Optional[Enhet]: + """Get :class:`Enhet` given an organization number. - res.raise_for_status() + Returns :class:`None` if Enhet is gone or not found. + Returns :class:`Enhet` if Enhet is found. - return Enhet.from_json(res.json()) - except httpx.HTTPError as exc: - raise BrregRestError( - str(exc), - method=(exc.request.method if exc.request else None), - url=(str(exc.request.url) if exc.request else None), - status_code=(res.status_code if res else None), - ) from exc - except Exception as exc: - raise BrregError(exc) from exc + Raises :class:`BrregRestError` if a REST error occurs. + Raises :class:`BrregError` if an unhandled exception occurs. + """ + res: Optional[httpx.Response] = None + try: + res = self.client.get(f"/enheter/{organisasjonsnummer}") + if res.status_code in (404, 410): + return None + res.raise_for_status() + return Enhet.from_json(res.json()) + except httpx.HTTPError as exc: + raise BrregRestError( + str(exc), + method=(exc.request.method if exc.request else None), + url=(str(exc.request.url) if exc.request else None), + status_code=(res.status_code if res else None), + ) from exc + except Exception as exc: + raise BrregError(exc) from exc diff --git a/tests/test_enhetsregisteret.py b/tests/test_enhetsregisteret.py index 0bf1455..1ad8213 100644 --- a/tests/test_enhetsregisteret.py +++ b/tests/test_enhetsregisteret.py @@ -19,7 +19,7 @@ def test_get_enhet( content=organization_details_response, ) - org = enhetsregisteret.get_enhet("818511752") + org = enhetsregisteret.Client().get_enhet("818511752") assert org is not None assert org.organisasjonsnummer == "818511752" @@ -65,7 +65,7 @@ def test_get_enhet_when_deleted( content=deleted_organization_details_response, ) - org = enhetsregisteret.get_enhet("815597222") + org = enhetsregisteret.Client().get_enhet("815597222") assert org is not None assert org.organisasjonsnummer == "815597222" @@ -86,7 +86,7 @@ def test_get_enhet_when_gone( headers={"content-type": "application/json"}, ) - org = enhetsregisteret.get_enhet("818511752") + org = enhetsregisteret.Client().get_enhet("818511752") assert org is None @@ -101,7 +101,7 @@ def test_get_enhet_when_not_found( headers={"content-type": "application/json"}, ) - org = enhetsregisteret.get_enhet("818511752") + org = enhetsregisteret.Client().get_enhet("818511752") assert org is None @@ -117,7 +117,7 @@ def test_get_enhet_when_http_error( ) with pytest.raises(BrregRestError) as exc_info: - enhetsregisteret.get_enhet("818511752") + enhetsregisteret.Client().get_enhet("818511752") assert "REST API exception" in str(exc_info.value) assert "Bad Request" in str(exc_info.value) @@ -138,7 +138,7 @@ def test_get_organization_by_number_when_http_timeout( ) with pytest.raises(BrregRestError) as exc_info: - enhetsregisteret.get_enhet("818511752") + enhetsregisteret.Client().get_enhet("818511752") assert "REST API exception" in str(exc_info.value) assert "Connection refused" in str(exc_info.value)