diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 03c669f..f1a9068 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -6,13 +6,19 @@ The following examples takes you through typical use cases with the :mod:`brreg` library. -Organization details by organization number -=========================================== +Querying Enhetsregisteret +========================= -To get details about an organization ("enhet") given its organization number: +To query Enhetsregisteret, you need to create a :class:`brreg.enhetsregisteret.Client` instance: >>> from brreg.enhetsregisteret import Client >>> client = Client() +>>> + +The client instance will ensure that the HTTP connection is reused across requests. + +To get details about an organization ("enhet") given its organization number: + >>> enhet = client.get_enhet('915501680') >>> enhet.organisasjonsnummer '915501680' @@ -21,5 +27,16 @@ To get details about an organization ("enhet") given its organization number: >>> enhet.organisasjonsform Organisasjonsform(kode='ASA', beskrivelse='Allmennaksjeselskap') >>> enhet.forretningsadresse -Adresse(land='Norge', landkode='NO', postnummer='0181', poststed='OSLO', adresse=['Torggata 7'], kommune='OSLO', kommunenummer='0301') +Adresse(adresse=['Torggata 7'], postnummer='0181', poststed='OSLO', kommunenummer='0301', kommune='OSLO', landkode='NO', land='Norge') +>>> + +To get details of a suborganization ("underenhet") given its organization number: + +>>> underenhet = client.get_underenhet('915659683') +>>> underenhet.organisasjonsnummer +'915659683' +>>> underenhet.antall_ansatte +91 +>>> underenhet.beliggenhetsadresse +Adresse(adresse=['Torggata 7'], postnummer='0181', poststed='OSLO', kommunenummer='0301', kommune='OSLO', landkode='NO', land='Norge') >>> diff --git a/src/brreg/enhetsregisteret/__init__.py b/src/brreg/enhetsregisteret/__init__.py index 7148bcc..a97313b 100644 --- a/src/brreg/enhetsregisteret/__init__.py +++ b/src/brreg/enhetsregisteret/__init__.py @@ -10,6 +10,7 @@ InstitusjonellSektorkode, Naeringskode, Organisasjonsform, + Underenhet, ) __all__ = [ @@ -21,4 +22,5 @@ "InstitusjonellSektorkode", "Naeringskode", "Organisasjonsform", + "Underenhet", ] diff --git a/src/brreg/enhetsregisteret/_client.py b/src/brreg/enhetsregisteret/_client.py index eb95c6b..e084dcb 100644 --- a/src/brreg/enhetsregisteret/_client.py +++ b/src/brreg/enhetsregisteret/_client.py @@ -1,16 +1,33 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from contextlib import contextmanager +from typing import TYPE_CHECKING, Any, Generator, Optional import httpx from brreg import BrregError, BrregRestError -from brreg.enhetsregisteret._types import Enhet +from brreg.enhetsregisteret._types import Enhet, Underenhet if TYPE_CHECKING: from types import TracebackType +@contextmanager +def error_handler() -> Generator[None, Any, None]: + try: + yield + except httpx.HTTPError as exc: + response: httpx.Response | None = getattr(exc, "response", None) + 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=(response.status_code if response else None), + ) from exc + except Exception as exc: + raise BrregError(exc) from exc + + class Client: _client: httpx.Client @@ -37,16 +54,8 @@ def close(self) -> None: self._client.close() def get_enhet(self, organisasjonsnummer: str) -> Optional[Enhet]: - """Get :class:`Enhet` given an organization number. - - Returns :class:`None` if Enhet is gone or not found. - Returns :class:`Enhet` if Enhet is found. - - Raises :class:`BrregRestError` if a REST error occurs. - Raises :class:`BrregError` if an unhandled exception occurs. - """ - res: Optional[httpx.Response] = None - try: + """Get :class:`Enhet` given an organization number.""" + with error_handler(): res = self._client.get( f"/enheter/{organisasjonsnummer}", headers={ @@ -57,12 +66,20 @@ def get_enhet(self, organisasjonsnummer: str) -> Optional[Enhet]: return None res.raise_for_status() return Enhet.model_validate_json(res.content) - 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 + + def get_underenhet(self, organisasjonsnummer: str) -> Optional[Underenhet]: + """Get :class:`Underenhet` given an organization number.""" + with error_handler(): + res = self._client.get( + f"/underenheter/{organisasjonsnummer}", + headers={ + "accept": ( + "application/vnd.brreg.enhetsregisteret.underenhet.v2+json;" + "charset=UTF-8" + ) + }, + ) + if res.status_code in (404, 410): + return None + res.raise_for_status() + return Underenhet.model_validate_json(res.content) diff --git a/src/brreg/enhetsregisteret/_types.py b/src/brreg/enhetsregisteret/_types.py index a3f6470..b2285be 100644 --- a/src/brreg/enhetsregisteret/_types.py +++ b/src/brreg/enhetsregisteret/_types.py @@ -197,3 +197,74 @@ class Enhet(BaseModel): #: Dato under-/enheten ble slettet slettedato: DateOrNone = None + + +class Underenhet(BaseModel): + """Enhet på laveste nivå i registreringsstrukturen i Enhetsregisteret. + + En underenhet kan ikke eksistere alene og har alltid knytning til en + hovedenhet. Identifiseres med organisasjonsnummer. + """ + + model_config = ConfigDict(alias_generator=to_camel) + + #: Underenhetens organisasjonsnummer + organisasjonsnummer: str + + #: Underenhetens navn + navn: str + + #: Underenhetens navn + organisasjonsform: Organisasjonsform + + #: Underenhetens hjemmeside + hjemmeside: Optional[str] = None + + #: Underenhetens postadresse + postadresse: Optional[Adresse] = None + + #: Underenhetens registreringsdato i Enhetsregisteret + registreringsdato_enhetsregisteret: DateOrNone = None + + #: Hvorvidt underenheten er registrert i MVA-registeret + registrert_i_mvaregisteret: Optional[bool] = None + + #: Underenheter som i utgangspunktet ikke er mva-pliktig, kan søke om + #: frivillig registrering i Merverdiavgiftsregisteret + frivillig_mva_registrert_beskrivelser: List[str] = Field(default_factory=list) + + #: Næringskode 1 + naeringskode1: Optional[Naeringskode] = None + + #: Næringskode 2 + naeringskode2: Optional[Naeringskode] = None + + #: Næringskode 3 + naeringskode3: Optional[Naeringskode] = None + + #: Hjelpeenhetskode + hjelpeenhetskode: Optional[Naeringskode] = None + + #: Antall ansatte + antall_ansatte: Optional[int] = None + + #: Angir om enheten har registrert ansatte + har_registrert_antall_ansatte: Optional[bool] = None + + #: Underenhetens overordnede enhet + overordnet_enhet: Optional[str] = None + + #: Underenhetens beliggenhetsadresse + beliggenhetsadresse: Optional[Adresse] = None + + #: Underenhetens oppstartsdato + oppstartsdato: DateOrNone = None + + #: Underenhetens dato for eierskifte + dato_eierskifte: DateOrNone = None + + #: Nedleggelsesdato for underenheten + nedleggelsesdato: DateOrNone = None + + #: Dato under-/enheten ble slettet + slettedato: DateOrNone = None diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index b669848..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,17 +0,0 @@ -from pathlib import Path - -import pytest - -TEST_DIR = Path(__file__).parent - - -@pytest.fixture() -def organization_details_response() -> bytes: - filepath = TEST_DIR / "data" / "organization-details-response.json" - return filepath.read_bytes() - - -@pytest.fixture() -def deleted_organization_details_response() -> bytes: - filepath = TEST_DIR / "data" / "deleted-organization-details-response.json" - return filepath.read_bytes() diff --git a/tests/data/deleted-organization-details-response.json b/tests/data/enheter-details-deleted-response.json similarity index 100% rename from tests/data/deleted-organization-details-response.json rename to tests/data/enheter-details-deleted-response.json diff --git a/tests/data/organization-details-response.json b/tests/data/enheter-details-response.json similarity index 100% rename from tests/data/organization-details-response.json rename to tests/data/enheter-details-response.json diff --git a/tests/data/underenheter-details-deleted-response.json b/tests/data/underenheter-details-deleted-response.json new file mode 100644 index 0000000..c0964e8 --- /dev/null +++ b/tests/data/underenheter-details-deleted-response.json @@ -0,0 +1,20 @@ +{ + "organisasjonsnummer": "987123456", + "navn": "SLETTET UNDERENHET AS", + "organisasjonsform": { + "kode": "AAFY", + "beskrivelse": "Virksomhet til ikke-næringsdrivende person", + "_links": { + "self": { + "href": "http://localhost/enhetsregisteret/api/organisasjonsformer/AAFY" + } + } + }, + "slettedato": "2017-10-20", + "nedleggelsesdato": "2017-10-05", + "_links": { + "self": { + "href": "http://localhost/enhetsregisteret/api/underenheter/987123456" + } + } +} diff --git a/tests/data/underenheter-details-response.json b/tests/data/underenheter-details-response.json new file mode 100644 index 0000000..1d4a54c --- /dev/null +++ b/tests/data/underenheter-details-response.json @@ -0,0 +1,49 @@ +{ + "organisasjonsnummer": "776655441", + "navn": "SESAM STASJON", + "organisasjonsform": { + "kode": "BEDR", + "beskrivelse": "Bedrift", + "_links": { + "self": { + "href": "http://localhost/enhetsregisteret/api/organisasjonsformer/BEDR" + } + } + }, + "postadresse": { + "land": "Norge", + "landkode": "NO", + "postnummer": "0122", + "poststed": "OSLO", + "adresse": ["c/o reder K. Rusing", "Postboks 1752 Vika", ""], + "kommune": "OSLO", + "kommunenummer": "0301" + }, + "registreringsdatoEnhetsregisteret": "2017-10-20", + "registrertIMvaregisteret": true, + "naeringskode1": { + "kode": "52.292", + "beskrivelse": "Skipsmegling" + }, + "antallAnsatte": 50, + "harRegistrertAntallAnsatte": true, + "overordnetEnhet": "112233445", + "beliggenhetsadresse": { + "land": "Norge", + "landkode": "NO", + "postnummer": "0122", + "poststed": "OSLO", + "adresse": ["Tyvholmen 1", null, null], + "kommune": "OSLO", + "kommunenummer": "0301" + }, + "nedleggelsesdato": "2018-10-20", + "_links": { + "self": { + "href": "http://localhost/enhetsregisteret/api/underenheter/776655441" + }, + "overordnetEnhet": { + "href": "http://localhost/enhetsregisteret/api/enheter/112233445" + } + } +} diff --git a/tests/enhetsregisteret/__init__.py b/tests/enhetsregisteret/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_enhetsregisteret.py b/tests/enhetsregisteret/test_get_enhet.py similarity index 87% rename from tests/test_enhetsregisteret.py rename to tests/enhetsregisteret/test_get_enhet.py index 6912bd2..e7754ca 100644 --- a/tests/test_enhetsregisteret.py +++ b/tests/enhetsregisteret/test_get_enhet.py @@ -1,4 +1,5 @@ from datetime import date +from pathlib import Path import httpx import pytest @@ -6,17 +7,16 @@ from brreg import BrregRestError, enhetsregisteret +DATA_DIR = Path(__file__).parent.parent / "data" -def test_get_enhet( - httpx_mock: HTTPXMock, - organization_details_response: bytes, -) -> None: + +def test_get_enhet(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( # pyright: ignore[reportUnknownMemberType] method="GET", url="https://data.brreg.no/enhetsregisteret/api/enheter/112233445", status_code=200, headers={"content-type": "application/json"}, - content=organization_details_response, + content=(DATA_DIR / "enheter-details-response.json").read_bytes(), ) org = enhetsregisteret.Client().get_enhet("112233445") @@ -54,16 +54,13 @@ def test_get_enhet( assert org.slettedato is None -def test_get_enhet_when_deleted( - httpx_mock: HTTPXMock, - deleted_organization_details_response: bytes, -) -> None: +def test_get_enhet_when_deleted(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( # pyright: ignore[reportUnknownMemberType] method="GET", url="https://data.brreg.no/enhetsregisteret/api/enheter/123456789", status_code=200, headers={"content-type": "application/json"}, - content=deleted_organization_details_response, + content=(DATA_DIR / "enheter-details-deleted-response.json").read_bytes(), ) org = enhetsregisteret.Client().get_enhet("123456789") @@ -79,9 +76,7 @@ def test_get_enhet_when_deleted( assert org.slettedato == date(2017, 10, 20) -def test_get_enhet_when_gone( - httpx_mock: HTTPXMock, -) -> None: +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", @@ -94,9 +89,7 @@ def test_get_enhet_when_gone( assert org is None -def test_get_enhet_when_not_found( - httpx_mock: HTTPXMock, -) -> None: +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", @@ -109,9 +102,7 @@ def test_get_enhet_when_not_found( assert org is None -def test_get_enhet_when_http_error( - httpx_mock: HTTPXMock, -) -> None: +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", @@ -133,9 +124,7 @@ def test_get_enhet_when_http_error( assert exc_info.value.status_code == 400 -def test_get_organization_by_number_when_http_timeout( - httpx_mock: HTTPXMock, -) -> 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"), ) diff --git a/tests/enhetsregisteret/test_get_underenhet.py b/tests/enhetsregisteret/test_get_underenhet.py new file mode 100644 index 0000000..dd6fc84 --- /dev/null +++ b/tests/enhetsregisteret/test_get_underenhet.py @@ -0,0 +1,67 @@ +from datetime import date +from pathlib import Path + +from pytest_httpx import HTTPXMock + +from brreg import enhetsregisteret + +DATA_DIR = Path(__file__).parent.parent / "data" + + +def test_get_underenhet(httpx_mock: HTTPXMock) -> None: + httpx_mock.add_response( # pyright: ignore[reportUnknownMemberType] + method="GET", + url="https://data.brreg.no/enhetsregisteret/api/underenheter/776655441", + status_code=200, + headers={"content-type": "application/json"}, + content=(DATA_DIR / "underenheter-details-response.json").read_bytes(), + ) + + org = enhetsregisteret.Client().get_underenhet("776655441") + + assert org is not None + assert org.organisasjonsnummer == "776655441" + assert org.navn == "SESAM STASJON" + assert org.hjemmeside is None + assert org.registreringsdato_enhetsregisteret == date(2017, 10, 20) + assert org.registrert_i_mvaregisteret is True + assert org.naeringskode1 == enhetsregisteret.Naeringskode( + kode="52.292", beskrivelse="Skipsmegling" + ) + assert org.antall_ansatte == 50 + assert org.har_registrert_antall_ansatte is True + assert org.beliggenhetsadresse == enhetsregisteret.Adresse( + land="Norge", + landkode="NO", + postnummer="0122", + poststed="OSLO", + adresse=["Tyvholmen 1", None, None], + kommune="OSLO", + kommunenummer="0301", + ) + assert org.oppstartsdato is None + assert org.dato_eierskifte is None + assert org.nedleggelsesdato == date(2018, 10, 20) + assert org.slettedato is None + + +def test_get_underenhet_when_deleted(httpx_mock: HTTPXMock) -> None: + httpx_mock.add_response( # pyright: ignore[reportUnknownMemberType] + method="GET", + url="https://data.brreg.no/enhetsregisteret/api/underenheter/987123456", + status_code=200, + headers={"content-type": "application/json"}, + content=(DATA_DIR / "underenheter-details-deleted-response.json").read_bytes(), + ) + + org = enhetsregisteret.Client().get_underenhet("987123456") + + assert org is not None + assert org.organisasjonsnummer == "987123456" + assert org.navn == "SLETTET UNDERENHET AS" + assert org.organisasjonsform == enhetsregisteret.Organisasjonsform( + kode="AAFY", + beskrivelse="Virksomhet til ikke-næringsdrivende person", + utgaatt=None, + ) + assert org.slettedato == date(2017, 10, 20)