Skip to content

Commit

Permalink
Merge pull request #11 from jodal/underenhet
Browse files Browse the repository at this point in the history
Add lookup of underenhet by org number
  • Loading branch information
jodal authored Nov 16, 2023
2 parents fde80f7 + 9cf1dd6 commit 5d7a6c3
Show file tree
Hide file tree
Showing 12 changed files with 279 additions and 64 deletions.
25 changes: 21 additions & 4 deletions docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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')
>>>
2 changes: 2 additions & 0 deletions src/brreg/enhetsregisteret/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
InstitusjonellSektorkode,
Naeringskode,
Organisasjonsform,
Underenhet,
)

__all__ = [
Expand All @@ -21,4 +22,5 @@
"InstitusjonellSektorkode",
"Naeringskode",
"Organisasjonsform",
"Underenhet",
]
59 changes: 38 additions & 21 deletions src/brreg/enhetsregisteret/_client.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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={
Expand All @@ -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)
71 changes: 71 additions & 0 deletions src/brreg/enhetsregisteret/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
17 changes: 0 additions & 17 deletions tests/conftest.py

This file was deleted.

File renamed without changes.
20 changes: 20 additions & 0 deletions tests/data/underenheter-details-deleted-response.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
49 changes: 49 additions & 0 deletions tests/data/underenheter-details-response.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
Empty file.
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
from datetime import date
from pathlib import Path

import httpx
import pytest
from pytest_httpx import HTTPXMock

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")
Expand Down Expand Up @@ -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")
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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"),
)
Expand Down
Loading

0 comments on commit 5d7a6c3

Please sign in to comment.