kvk-connect 0.1.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. kvk_connect/__init__.py +11 -0
  2. kvk_connect/api/__init__.py +4 -0
  3. kvk_connect/api/client.py +183 -0
  4. kvk_connect/api/endpoints.py +24 -0
  5. kvk_connect/api/session.py +34 -0
  6. kvk_connect/cli/main.py +26 -0
  7. kvk_connect/db/__init__.py +0 -0
  8. kvk_connect/db/basisprofiel_reader.py +67 -0
  9. kvk_connect/db/basisprofiel_writer.py +73 -0
  10. kvk_connect/db/init.py +25 -0
  11. kvk_connect/db/kvkvestigingen_reader.py +41 -0
  12. kvk_connect/db/kvkvestigingen_writer.py +73 -0
  13. kvk_connect/db/signaal_reader.py +23 -0
  14. kvk_connect/db/signaal_writer.py +73 -0
  15. kvk_connect/db/vestigingenprofiel_reader.py +66 -0
  16. kvk_connect/db/vestigingsprofiel_writer.py +92 -0
  17. kvk_connect/logging_config.py +27 -0
  18. kvk_connect/mappers/__init__.py +1 -0
  19. kvk_connect/mappers/kvk_record_mapper.py +100 -0
  20. kvk_connect/mappers/map_mutatie_abonnement_api_to_mutatieabonnement.py +11 -0
  21. kvk_connect/mappers/map_vestigingen_api_to_vestigingsnummers.py +14 -0
  22. kvk_connect/mappers/map_vestigingsprofiel_api_to_vestigingsprofiel_domain.py +41 -0
  23. kvk_connect/models/__init__.py +0 -0
  24. kvk_connect/models/api/__init__.py +0 -0
  25. kvk_connect/models/api/abonnementen_api.py +42 -0
  26. kvk_connect/models/api/basisprofiel_api.py +233 -0
  27. kvk_connect/models/api/mutatie_abonnementen_api.py +40 -0
  28. kvk_connect/models/api/mutatiesignalen_api.py +44 -0
  29. kvk_connect/models/api/vestigingen_api.py +73 -0
  30. kvk_connect/models/api/vestigingsprofiel_api.py +71 -0
  31. kvk_connect/models/domain/__init__.py +6 -0
  32. kvk_connect/models/domain/basisprofiel.py +65 -0
  33. kvk_connect/models/domain/kvkvestigingsnummersdomain.py +28 -0
  34. kvk_connect/models/domain/mutatie_abonnement.py +20 -0
  35. kvk_connect/models/domain/vestigingsadresdomain.py +62 -0
  36. kvk_connect/models/domain/vestigingsadressendomain.py +48 -0
  37. kvk_connect/models/domain/vestigingsprofiel_domain.py +58 -0
  38. kvk_connect/models/orm/base.py +5 -0
  39. kvk_connect/models/orm/basisprofiel_orm.py +52 -0
  40. kvk_connect/models/orm/kvkvestigingen_orm.py +53 -0
  41. kvk_connect/models/orm/signaal_orm.py +40 -0
  42. kvk_connect/models/orm/vestigingsprofiel_orm.py +58 -0
  43. kvk_connect/services/__init__.py +4 -0
  44. kvk_connect/services/record_service.py +66 -0
  45. kvk_connect/utils/__init__.py +5 -0
  46. kvk_connect/utils/env.py +16 -0
  47. kvk_connect/utils/formatting.py +11 -0
  48. kvk_connect/utils/rate_limit.py +21 -0
  49. kvk_connect/utils/tools.py +131 -0
  50. kvk_connect-0.1.6.dist-info/METADATA +352 -0
  51. kvk_connect-0.1.6.dist-info/RECORD +52 -0
  52. kvk_connect-0.1.6.dist-info/WHEEL +4 -0
@@ -0,0 +1,66 @@
1
+ from sqlalchemy import select
2
+ from sqlalchemy.orm import Session
3
+
4
+ from kvk_connect.models.orm.kvkvestigingen_orm import VestigingenORM
5
+ from kvk_connect.models.orm.signaal_orm import SignaalORM
6
+ from kvk_connect.models.orm.vestigingsprofiel_orm import VestigingsProfielORM
7
+
8
+
9
+ class VestigingsProfielReader:
10
+ def __init__(self, engine):
11
+ self.engine = engine
12
+
13
+ def get_vestigingen_zonder_vestigingsprofielen(self) -> list[str]:
14
+ """Haalt alle vestigingsnummers op die wel in kvk_vestigingen staan maar nog niet in vestigingsprofielen.
15
+
16
+ Returns:
17
+ list[str]: Lijst met vestigingsnummers zonder vestigingsprofiel
18
+ """
19
+ with Session(self.engine) as session:
20
+ stmt = (
21
+ select(VestigingenORM.vestigingsnummer)
22
+ .outerjoin(
23
+ VestigingsProfielORM, VestigingenORM.vestigingsnummer == VestigingsProfielORM.vestigingsnummer
24
+ )
25
+ .where(VestigingsProfielORM.vestigingsnummer.is_(None))
26
+ .where(VestigingenORM.vestigingsnummer != VestigingenORM.SENTINEL_VESTIGINGSNUMMER)
27
+ .distinct()
28
+ )
29
+
30
+ result = session.execute(stmt)
31
+ return [row[0] for row in result.fetchall()]
32
+
33
+ def get_outdated_vestigingen(self) -> list[str]:
34
+ """Return lijst van vestigingsnummers met vestiging nieuwer dan de lastupdated van het vestigingenprofiel."""
35
+
36
+ with Session(self.engine) as session:
37
+ stmt = (
38
+ select(VestigingenORM.vestigingsnummer)
39
+ .join(VestigingsProfielORM, VestigingenORM.vestigingsnummer == VestigingsProfielORM.vestigingsnummer)
40
+ .where(VestigingenORM.last_updated > VestigingsProfielORM.last_updated)
41
+ .where(VestigingenORM.vestigingsnummer != VestigingenORM.SENTINEL_VESTIGINGSNUMMER)
42
+ .distinct()
43
+ )
44
+
45
+ result = session.execute(stmt).scalars().all()
46
+ return list(result)
47
+
48
+ def get_outdated_vestigingen_signaal(self) -> list[str]:
49
+ """Return lijst van vestigingsnummers met signaal nieuwer is dan de last_updated van het vestigingenprofiel.
50
+
51
+ Returns:
52
+ list[str]: Lijst met vestigingsnummers die een update nodig hebben
53
+ """
54
+ with Session(self.engine) as session:
55
+ stmt = (
56
+ select(SignaalORM.vestigingsnummer)
57
+ .join(VestigingsProfielORM, SignaalORM.vestigingsnummer == VestigingsProfielORM.vestigingsnummer)
58
+ .where(
59
+ SignaalORM.timestamp > VestigingsProfielORM.last_updated,
60
+ SignaalORM.vestigingsnummer.is_not(None), # Alleen vestigingsprofielen, geen basisprofielen
61
+ )
62
+ .distinct()
63
+ )
64
+
65
+ result = session.execute(stmt).scalars().all()
66
+ return [v for v in result if v is not None] # filter out possible None values
@@ -0,0 +1,92 @@
1
+ # ruff: noqa: D102
2
+ import logging
3
+ from datetime import UTC, datetime
4
+
5
+ from sqlalchemy.orm import Session, sessionmaker
6
+
7
+ from kvk_connect.models.domain.vestigingsprofiel_domain import VestigingsProfielDomain
8
+ from kvk_connect.models.orm.vestigingsprofiel_orm import VestigingsProfielORM
9
+ from kvk_connect.utils.tools import parse_kvk_datum
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class VestigingsProfielWriter:
15
+ def __init__(self, engine, batch_size: int = 1):
16
+ logger.info("Initializing BasisProfielWriter, met batch size: %d", batch_size)
17
+ self.Session = sessionmaker(bind=engine)
18
+ self.batch_size = batch_size
19
+ self._session: Session | None = None
20
+ self._count = 0
21
+
22
+ def __enter__(self):
23
+ """Start een nieuwe database sessie."""
24
+ self._session = self.Session()
25
+ return self
26
+
27
+ def __exit__(self, exc_type, exc, tb):
28
+ """Commit changes on successful exit, rollback on exception."""
29
+ try:
30
+ if exc is None:
31
+ self.flush()
32
+ else:
33
+ if self._session:
34
+ self._session.rollback()
35
+ finally:
36
+ if self._session:
37
+ self._session.close()
38
+ self._session = None
39
+
40
+ def flush(self) -> None:
41
+ if self._session:
42
+ self._session.commit()
43
+
44
+ def add(self, domain_vestigingsprofiel: VestigingsProfielDomain) -> None:
45
+ if not self._session:
46
+ raise RuntimeError("Session not initialized. Use context manager.")
47
+
48
+ orm_obj = self._to_orm(domain_vestigingsprofiel)
49
+ orm_obj.last_updated = datetime.now(UTC)
50
+
51
+ self._session.merge(orm_obj)
52
+ self._count += 1
53
+
54
+ if self._count % self.batch_size == 0:
55
+ self._session.commit()
56
+
57
+ @staticmethod
58
+ def _to_orm(domein_obj: VestigingsProfielDomain) -> VestigingsProfielORM:
59
+ # Convert GPS coordinates from string to float (handle comma decimal separator)
60
+ gps_lat = None
61
+ gps_lon = None
62
+
63
+ if domein_obj.bzk_adres_gps_latitude:
64
+ try:
65
+ gps_lat = float(domein_obj.bzk_adres_gps_latitude.replace(",", "."))
66
+ except (ValueError, AttributeError):
67
+ logger.warning("Invalid latitude value: %s", domein_obj.bzk_adres_gps_latitude)
68
+
69
+ if domein_obj.bzk_adres_gps_longitude:
70
+ try:
71
+ gps_lon = float(domein_obj.bzk_adres_gps_longitude.replace(",", "."))
72
+ except (ValueError, AttributeError):
73
+ logger.warning("Invalid longitude value: %s", domein_obj.bzk_adres_gps_longitude)
74
+
75
+ return VestigingsProfielORM(
76
+ vestigingsnummer=domein_obj.vestigingsnummer,
77
+ cor_adres_volledig=domein_obj.cor_adres_volledig,
78
+ cor_adres_postcode=domein_obj.cor_adres_postcode,
79
+ cor_adres_postbusnummer=domein_obj.cor_adres_postbusnummer,
80
+ cor_adres_plaats=domein_obj.cor_adres_plaats,
81
+ cor_adres_land=domein_obj.cor_adres_land,
82
+ bzk_adres_volledig=domein_obj.bzk_adres_volledig,
83
+ bzk_adres_straatnaam=domein_obj.bzk_adres_straatnaam,
84
+ bzk_adres_huisnummer=domein_obj.bzk_adres_huisnummer,
85
+ bzk_adres_postcode=domein_obj.bzk_adres_postcode,
86
+ bzk_adres_plaats=domein_obj.bzk_adres_plaats,
87
+ bzk_adres_land=domein_obj.bzk_adres_land,
88
+ bzk_adres_gps_latitude=gps_lat,
89
+ bzk_adres_gps_longitude=gps_lon,
90
+ registratie_datum_aanvang_vestiging=parse_kvk_datum(domein_obj.registratie_datum_aanvang_vestiging),
91
+ registratie_datum_einde_vestiging=parse_kvk_datum(domein_obj.registratie_datum_einde_vestiging),
92
+ )
@@ -0,0 +1,27 @@
1
+ import logging
2
+ from datetime import UTC, datetime
3
+
4
+
5
+ class LocalTimezoneFormatter(logging.Formatter):
6
+ """Formatter that converts UTC timestamps to local system timezone."""
7
+
8
+ def formatTime(self, record, datefmt=None): # noqa: N802, overrides base method
9
+ """Convert UTC timestamp to timezone-aware datetime, then to local time."""
10
+ dt_utc = datetime.fromtimestamp(record.created, tz=UTC)
11
+ dt_local = dt_utc.astimezone()
12
+
13
+ if datefmt:
14
+ return dt_local.strftime(datefmt)
15
+ else:
16
+ return dt_local.strftime("%Y-%m-%d %H:%M:%S")
17
+
18
+
19
+ def configure(level: int = logging.INFO):
20
+ """Configure logging with local timezone formatter."""
21
+ handler = logging.StreamHandler()
22
+ formatter = LocalTimezoneFormatter(
23
+ fmt="%(asctime)s [%(levelname)s] %(name)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
24
+ )
25
+ handler.setFormatter(formatter)
26
+
27
+ logging.basicConfig(level=level, handlers=[handler], force=True)
@@ -0,0 +1 @@
1
+ # mappers package initialization
@@ -0,0 +1,100 @@
1
+ from __future__ import annotations
2
+
3
+ from kvk_connect.models.api.basisprofiel_api import Adres, BasisProfielAPI, Hoofdvestiging, SBIActiviteit
4
+ from kvk_connect.models.domain import BasisProfielDomain
5
+ from kvk_connect.utils.tools import clean_and_pad, formatteer_datum, truncate_float
6
+
7
+
8
+ def _select_adres(adressen: list[Adres] | None) -> Adres | None:
9
+ if not adressen:
10
+ return None
11
+ # Prefer "bezoekadres", else fall back to "correspondentieadres", else first
12
+ bezoek = next((a for a in adressen if a and a.type == "bezoekadres"), None)
13
+ if bezoek:
14
+ return bezoek
15
+ corr = next((a for a in adressen if a and a.type == "correspondentieadres"), None)
16
+ return corr or adressen[0]
17
+
18
+
19
+ def _format_straatnaam(adres: Adres) -> str:
20
+ if not adres or not adres.straatnaam:
21
+ return ""
22
+ parts = [adres.straatnaam]
23
+ if adres.huisnummer is not None:
24
+ parts.append(str(adres.huisnummer))
25
+ # The parser used 'toevoegingAdres'; our model has 'huisletter'. Prefer huisletter if present.
26
+ if adres.huisletter:
27
+ parts[-1] = f"{parts[-1]}{adres.huisletter}" if parts else adres.huisletter
28
+ return " ".join(parts)
29
+
30
+
31
+ def _map_sbi(api: BasisProfielAPI) -> tuple[str, str, str]:
32
+ hoofd_code = ""
33
+ hoofd_oms = ""
34
+ overige_codes: list[str] = []
35
+
36
+ activiteiten: list[SBIActiviteit] = api.sbi_activiteiten or []
37
+ for act in activiteiten:
38
+ if (act.ind_hoofdactiviteit or "").lower() == "ja":
39
+ hoofd_code = clean_and_pad(act.sbi_code, 5) if act.sbi_code else ""
40
+ hoofd_oms = act.sbi_omschrijving or ""
41
+ else:
42
+ if act.sbi_code:
43
+ overige_codes.append(clean_and_pad(act.sbi_code, 5))
44
+
45
+ return hoofd_code, hoofd_oms, ", ".join(overige_codes)
46
+
47
+
48
+ def _map_hoofdvestiging(hv: Hoofdvestiging | None, out: BasisProfielDomain) -> None:
49
+ if not hv:
50
+ return
51
+
52
+ out.eerste_handelsnaam = hv.eerste_handelsnaam or ""
53
+ out.vestigingsnummer = hv.vestigingsnummer or ""
54
+ out.totaal_werkzame_personen = hv.totaal_werkzame_personen if hv.totaal_werkzame_personen is not None else None
55
+ out.websites = ", ".join(hv.websites or [])
56
+
57
+ # Adres
58
+ adres = _select_adres(hv.adressen or [])
59
+ if adres:
60
+ out.adres_type = adres.type or ""
61
+ # The Adres model has no 'postbusnummer' field; keep empty to match parser behavior when absent.
62
+ out.postbusnummer = ""
63
+ out.adres_straatnaam = _format_straatnaam(adres)
64
+ out.adres_toevoeging = "" # parser used 'toevoegingAdres', not present in model; keep empty
65
+ out.adres_postcode = adres.postcode or ""
66
+ out.adres_plaats = adres.plaats or ""
67
+ if adres.geo_data:
68
+ out.gps_latitude = truncate_float(adres.geo_data.gps_latitude)
69
+ out.gps_longitude = truncate_float(adres.geo_data.gps_longitude)
70
+
71
+
72
+ def map_kvkbasisprofiel_api_to_kvkrecord(api: BasisProfielAPI) -> BasisProfielDomain:
73
+ """Map a KVKRecordAPI model (Basisprofiel) to a KVKRecord (BasisprofielOutput).
74
+
75
+ This function is mirroring the logic from parsers.kvkparser.parse_basisprofiel.
76
+ """
77
+ out = BasisProfielDomain()
78
+
79
+ # Top-level
80
+ out.kvk_nummer = api.kvk_nummer or ""
81
+ out.naam = api.naam or ""
82
+
83
+ # SBI activiteiten
84
+ out.hoofdactiviteit, out.hoofdactiviteit_omschrijving, out.activiteit_overig = _map_sbi(api)
85
+
86
+ # Eigenaar
87
+ if api.embedded and api.embedded.eigenaar:
88
+ out.rechtsvorm = api.embedded.eigenaar.rechtsvorm or ""
89
+ out.rechtsvorm_uitgebreid = api.embedded.eigenaar.uitgebreide_rechtsvorm or ""
90
+
91
+ # Registratie
92
+ if api.materiele_registratie:
93
+ out.registratie_datum_aanvang = formatteer_datum(str(api.materiele_registratie.datum_aanvang or ""))
94
+ out.registratie_datum_einde = formatteer_datum(str(api.materiele_registratie.datum_einde or ""))
95
+
96
+ # Hoofdvestiging
97
+ hv = api.embedded.hoofdvestiging if api.embedded else None
98
+ _map_hoofdvestiging(hv, out)
99
+
100
+ return out
@@ -0,0 +1,11 @@
1
+ from kvk_connect.models.api.mutatie_abonnementen_api import MutatieAbonnementenAPI
2
+ from kvk_connect.models.domain.mutatie_abonnement import MutatieAbonnementDomain
3
+
4
+
5
+ def map_mutatie_abonnement_api_to_mutatieabonnement(api_model: MutatieAbonnementenAPI) -> MutatieAbonnementDomain:
6
+ """Maps MutatieAbonnementenAPI to MutatieAbonnement.
7
+
8
+ Extracts abonnement IDs from the API model.
9
+ """
10
+ abonnement_ids = [abonnement.id for abonnement in api_model.abonnementen]
11
+ return MutatieAbonnementDomain(abonnement_ids=abonnement_ids)
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from ..models.api.vestigingen_api import VestigingenAPI
4
+ from ..models.domain import KvKVestigingsNummersDomain
5
+
6
+
7
+ def map_vestigingen_api_to_vestigingsnummers(api_model: VestigingenAPI) -> KvKVestigingsNummersDomain:
8
+ """Zet een VestigingenAPI (API-model) om naar ons domeinmodel KvKVestigingsnummers.
9
+
10
+ Haalt de lijst van 'vestigingsnummer' waarden op in de originele volgorde.
11
+ """
12
+ nummers: list[str] = [v.vestigingsnummer for v in (api_model.vestigingen or []) if v and v.vestigingsnummer]
13
+
14
+ return KvKVestigingsNummersDomain(kvk_nummer=api_model.kvk_nummer or "", vestigingsnummers=nummers)
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ from kvk_connect.models.api.vestigingsprofiel_api import VestigingsProfielAPI
4
+ from kvk_connect.models.domain.vestigingsprofiel_domain import VestigingsProfielDomain
5
+ from kvk_connect.utils.tools import formatteer_datum, truncate_float
6
+
7
+
8
+ def map_vestigingsprofiel_api_to_vestigingsprofiel_domain(api_model: VestigingsProfielAPI) -> VestigingsProfielDomain:
9
+ """Zet een VestigingsProfielAPI (API-model) om naar een VestigingsProfielDomain.
10
+
11
+ Extracteert correspondentieadres en bezoekadres uit de adressen lijst.
12
+ """
13
+ cor_adres = next((a for a in (api_model.adressen or []) if a.type == "correspondentieadres"), None)
14
+ bzk_adres = next((a for a in (api_model.adressen or []) if a.type == "bezoekadres"), None)
15
+
16
+ return VestigingsProfielDomain(
17
+ vestigingsnummer=api_model.vestigingsnummer if api_model.vestigingsnummer else None,
18
+ cor_adres_volledig=cor_adres.volledig_adres if cor_adres and cor_adres.volledig_adres else None,
19
+ cor_adres_postcode=cor_adres.postcode if cor_adres and cor_adres.postcode else None,
20
+ cor_adres_postbusnummer=getattr(cor_adres, "postbusnummer", None) if cor_adres else None,
21
+ cor_adres_plaats=cor_adres.plaats if cor_adres and cor_adres.plaats else None,
22
+ cor_adres_land=cor_adres.land if cor_adres and cor_adres.land else None,
23
+ bzk_adres_volledig=bzk_adres.volledig_adres if bzk_adres and bzk_adres.volledig_adres else None,
24
+ bzk_adres_straatnaam=bzk_adres.straatnaam if bzk_adres else None,
25
+ bzk_adres_huisnummer=getattr(bzk_adres, "huisnummer", None) if bzk_adres else None,
26
+ bzk_adres_postcode=bzk_adres.postcode if bzk_adres and bzk_adres.postcode else None,
27
+ bzk_adres_plaats=bzk_adres.plaats if bzk_adres and bzk_adres.plaats else None,
28
+ bzk_adres_land=bzk_adres.land if bzk_adres and bzk_adres.land else None,
29
+ bzk_adres_gps_latitude=truncate_float(bzk_adres.geo_data.gps_latitude)
30
+ if bzk_adres and bzk_adres.geo_data and bzk_adres.geo_data.gps_latitude
31
+ else None,
32
+ bzk_adres_gps_longitude=truncate_float(bzk_adres.geo_data.gps_longitude)
33
+ if bzk_adres and bzk_adres.geo_data and bzk_adres.geo_data.gps_longitude
34
+ else None,
35
+ registratie_datum_aanvang_vestiging=formatteer_datum(str(api_model.materiele_registratie.datum_aanvang or ""))
36
+ if api_model.materiele_registratie
37
+ else None,
38
+ registratie_datum_einde_vestiging=formatteer_datum(str(api_model.materiele_registratie.datum_einde or ""))
39
+ if api_model.materiele_registratie
40
+ else None,
41
+ )
File without changes
File without changes
@@ -0,0 +1,42 @@
1
+ # ruff: noqa: D102
2
+ from dataclasses import dataclass, field
3
+ from typing import Any
4
+
5
+
6
+ @dataclass
7
+ class Contract:
8
+ id: str
9
+
10
+ @classmethod
11
+ def from_dict(cls, data: dict[str, Any]) -> "Contract":
12
+ return cls(id=data.get("id", ""))
13
+
14
+
15
+ @dataclass
16
+ class Abonnement:
17
+ id: str
18
+ contract: Contract
19
+ start_datum: str
20
+ actief: bool
21
+
22
+ @classmethod
23
+ def from_dict(cls, data: dict[str, Any]) -> "Abonnement":
24
+ return cls(
25
+ id=data.get("id", ""),
26
+ contract=Contract.from_dict(data.get("contract", {})),
27
+ start_datum=data.get("startDatum", ""),
28
+ actief=data.get("actief", False),
29
+ )
30
+
31
+
32
+ @dataclass
33
+ class AbonnementenAPI:
34
+ klant_id: str
35
+ abonnementen: list[Abonnement] = field(default_factory=list)
36
+
37
+ @classmethod
38
+ def from_dict(cls, data: dict[str, Any]) -> "AbonnementenAPI":
39
+ return cls(
40
+ klant_id=data.get("klantId", ""),
41
+ abonnementen=[Abonnement.from_dict(a) for a in data.get("abonnementen", [])],
42
+ )
@@ -0,0 +1,233 @@
1
+ # ruff: noqa: D102
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from dataclasses import asdict, dataclass, field
6
+ from typing import Any
7
+
8
+ """
9
+ Naming conventions voor API models is appenden van *API
10
+ """
11
+
12
+
13
+ @dataclass
14
+ class Link:
15
+ rel: str = ""
16
+ href: str = ""
17
+
18
+ @staticmethod
19
+ def from_dict(d: dict[str, Any] | None) -> Link | None:
20
+ if not d:
21
+ return None
22
+ return Link(rel=d.get("rel", "") or "", href=d.get("href", "") or "")
23
+
24
+
25
+ @dataclass
26
+ class GeoData:
27
+ addresseerbaar_object_id: str | None = None
28
+ nummer_aanduiding_id: str | None = None
29
+ gps_latitude: float | None = None
30
+ gps_longitude: float | None = None
31
+ rijksdriehoek_x: float | None = None
32
+ rijksdriehoek_y: float | None = None
33
+ rijksdriehoek_z: float | None = None
34
+
35
+ @staticmethod
36
+ def from_dict(d: dict[str, Any] | None) -> GeoData | None:
37
+ if not d:
38
+ return None
39
+ return GeoData(
40
+ addresseerbaar_object_id=d.get("addresseerbaarObjectId"),
41
+ nummer_aanduiding_id=d.get("nummerAanduidingId"),
42
+ gps_latitude=d.get("gpsLatitude"),
43
+ gps_longitude=d.get("gpsLongitude"),
44
+ rijksdriehoek_x=d.get("rijksdriehoekX"),
45
+ rijksdriehoek_y=d.get("rijksdriehoekY"),
46
+ rijksdriehoek_z=d.get("rijksdriehoekZ"),
47
+ )
48
+
49
+
50
+ @dataclass
51
+ class Adres:
52
+ type: str = ""
53
+ ind_afgeschermd: str = ""
54
+ volledig_adres: str = ""
55
+ straatnaam: str = ""
56
+ huisnummer: int | None = None
57
+ huisletter: str | None = None
58
+ postbusnummer: int | None = None
59
+ postcode: str = ""
60
+ plaats: str = ""
61
+ land: str = ""
62
+ geo_data: GeoData | None = None
63
+
64
+ @staticmethod
65
+ def from_dict(d: dict[str, Any] | None) -> Adres | None:
66
+ if not d:
67
+ return None
68
+ return Adres(
69
+ type=d.get("type", "") or "",
70
+ ind_afgeschermd=d.get("indAfgeschermd", "") or "",
71
+ volledig_adres=d.get("volledigAdres", "") or "",
72
+ straatnaam=d.get("straatnaam", "") or "",
73
+ huisnummer=d.get("huisnummer"),
74
+ huisletter=d.get("huisletter"),
75
+ postbusnummer=d.get("postbusnummer"),
76
+ postcode=d.get("postcode", "") or "",
77
+ plaats=d.get("plaats", "") or "",
78
+ land=d.get("land", "") or "",
79
+ geo_data=GeoData.from_dict(d.get("geoData")),
80
+ )
81
+
82
+
83
+ @dataclass
84
+ class MaterieleRegistratie:
85
+ datum_aanvang: str | None = None
86
+ datum_einde: str | None = None
87
+
88
+ @staticmethod
89
+ def from_dict(d: dict[str, Any] | None) -> MaterieleRegistratie | None:
90
+ if not d:
91
+ return None
92
+ return MaterieleRegistratie(datum_aanvang=d.get("datumAanvang"), datum_einde=d.get("datumEinde"))
93
+
94
+
95
+ @dataclass
96
+ class HandelNaam:
97
+ naam: str = ""
98
+ volgorde: int | None = None
99
+
100
+ @staticmethod
101
+ def from_dict(d: dict[str, Any] | None) -> HandelNaam | None:
102
+ if not d:
103
+ return None
104
+ return HandelNaam(naam=d.get("naam", "") or "", volgorde=d.get("volgorde"))
105
+
106
+
107
+ @dataclass
108
+ class SBIActiviteit:
109
+ sbi_code: str = ""
110
+ sbi_omschrijving: str = ""
111
+ ind_hoofdactiviteit: str = ""
112
+
113
+ @staticmethod
114
+ def from_dict(d: dict[str, Any] | None) -> SBIActiviteit | None:
115
+ if not d:
116
+ return None
117
+ return SBIActiviteit(
118
+ sbi_code=d.get("sbiCode", "") or "",
119
+ sbi_omschrijving=d.get("sbiOmschrijving", "") or "",
120
+ ind_hoofdactiviteit=d.get("indHoofdactiviteit", "") or "",
121
+ )
122
+
123
+
124
+ @dataclass
125
+ class Hoofdvestiging:
126
+ vestigingsnummer: str = ""
127
+ kvk_nummer: str = ""
128
+ formele_registratiedatum: str = ""
129
+ materiele_registratie: MaterieleRegistratie | None = None
130
+ eerste_handelsnaam: str = ""
131
+ ind_hoofdvestiging: str = ""
132
+ ind_commerciele_vestiging: str = ""
133
+ totaal_werkzame_personen: int | None = None
134
+ adressen: list[Adres] = field(default_factory=list)
135
+ websites: list[str] = field(default_factory=list)
136
+ links: list[Link] = field(default_factory=list)
137
+
138
+ @staticmethod
139
+ def from_dict(d: dict[str, Any] | None) -> Hoofdvestiging | None: # noqa: D102
140
+ if not d:
141
+ return None
142
+ return Hoofdvestiging(
143
+ vestigingsnummer=d.get("vestigingsnummer", "") or "",
144
+ kvk_nummer=d.get("kvkNummer", "") or "",
145
+ formele_registratiedatum=d.get("formeleRegistratiedatum", "") or "",
146
+ materiele_registratie=MaterieleRegistratie.from_dict(d.get("materieleRegistratie")),
147
+ eerste_handelsnaam=d.get("eersteHandelsnaam", "") or "",
148
+ ind_hoofdvestiging=d.get("indHoofdvestiging", "") or "",
149
+ ind_commerciele_vestiging=d.get("indCommercieleVestiging", "") or "",
150
+ totaal_werkzame_personen=d.get("totaalWerkzamePersonen"),
151
+ adressen=[a for a in (Adres.from_dict(x) for x in d.get("adressen", [])) if a],
152
+ websites=list(d.get("websites", []) or []),
153
+ links=[link for link in (Link.from_dict(x) for x in d.get("links", [])) if link],
154
+ )
155
+
156
+
157
+ @dataclass
158
+ class Eigenaar:
159
+ rechtsvorm: str = ""
160
+ uitgebreide_rechtsvorm: str = ""
161
+ links: list[Link] = field(default_factory=list)
162
+
163
+ @staticmethod
164
+ def from_dict(d: dict[str, Any] | None) -> Eigenaar | None: # noqa: D102
165
+ if not d:
166
+ return None
167
+ return Eigenaar(
168
+ rechtsvorm=d.get("rechtsvorm", "") or "",
169
+ uitgebreide_rechtsvorm=d.get("uitgebreideRechtsvorm", "") or "",
170
+ links=[link for link in (Link.from_dict(x) for x in d.get("links", [])) if link],
171
+ )
172
+
173
+
174
+ @dataclass
175
+ class Embedded:
176
+ hoofdvestiging: Hoofdvestiging | None = None
177
+ eigenaar: Eigenaar | None = None
178
+
179
+ @staticmethod
180
+ def from_dict(d: dict[str, Any] | None) -> Embedded | None: # noqa: D102
181
+ if not d:
182
+ return None
183
+ return Embedded(
184
+ hoofdvestiging=Hoofdvestiging.from_dict(d.get("hoofdvestiging")),
185
+ eigenaar=Eigenaar.from_dict(d.get("eigenaar")),
186
+ )
187
+
188
+
189
+ @dataclass
190
+ class BasisProfielAPI:
191
+ kvk_nummer: str = ""
192
+ ind_non_mailing: str = ""
193
+ naam: str = ""
194
+ formele_registratiedatum: str = ""
195
+ materiele_registratie: MaterieleRegistratie | None = None
196
+ totaal_werkzame_personen: int | None = None
197
+ handelsnamen: list[HandelNaam] = field(default_factory=list)
198
+ sbi_activiteiten: list[SBIActiviteit] = field(default_factory=list)
199
+ links: list[Link] = field(default_factory=list)
200
+ embedded: Embedded | None = None
201
+
202
+ @staticmethod
203
+ def from_dict(d: dict[str, Any]) -> BasisProfielAPI: # noqa: D102
204
+ return BasisProfielAPI(
205
+ kvk_nummer=d.get("kvkNummer", "") or "",
206
+ ind_non_mailing=d.get("indNonMailing", "") or "",
207
+ naam=d.get("naam", "") or "",
208
+ formele_registratiedatum=d.get("formeleRegistratiedatum", "") or "",
209
+ materiele_registratie=MaterieleRegistratie.from_dict(d.get("materieleRegistratie")),
210
+ totaal_werkzame_personen=d.get("totaalWerkzamePersonen"),
211
+ handelsnamen=[h for h in (HandelNaam.from_dict(x) for x in d.get("handelsnamen", [])) if h],
212
+ sbi_activiteiten=[a for a in (SBIActiviteit.from_dict(x) for x in d.get("sbiActiviteiten", [])) if a],
213
+ links=[link for link in (Link.from_dict(x) for x in d.get("links", [])) if link],
214
+ embedded=Embedded.from_dict(d.get("_embedded")),
215
+ )
216
+
217
+ @staticmethod
218
+ def load_from_file(path: str, encoding: str = "utf-8") -> BasisProfielAPI: # noqa: D102
219
+ with open(path, encoding=encoding) as f:
220
+ data = json.load(f)
221
+ return BasisProfielAPI.from_dict(data)
222
+
223
+ @staticmethod
224
+ def load_from_json(json_str: str) -> BasisProfielAPI: # noqa: D102
225
+ data = json.loads(json_str)
226
+ return BasisProfielAPI.from_dict(data)
227
+
228
+ @staticmethod
229
+ def load_from_dict(data: dict[str, Any]) -> BasisProfielAPI: # noqa: D102
230
+ return BasisProfielAPI.from_dict(data)
231
+
232
+ def to_dict(self) -> dict[str, Any]: # noqa: D102
233
+ return asdict(self)
@@ -0,0 +1,40 @@
1
+ # ruff: noqa: D102
2
+ from dataclasses import dataclass
3
+
4
+
5
+ @dataclass
6
+ class Contract:
7
+ id: str
8
+
9
+ @staticmethod
10
+ def from_dict(data: dict) -> "Contract": # noqa: D102
11
+ return Contract(id=data["id"])
12
+
13
+
14
+ @dataclass
15
+ class Abonnement:
16
+ id: str
17
+ contract: Contract
18
+ start_datum: str
19
+ actief: bool
20
+
21
+ @staticmethod
22
+ def from_dict(data: dict) -> "Abonnement": # noqa: D102
23
+ return Abonnement(
24
+ id=data["id"],
25
+ contract=Contract.from_dict(data["contract"]),
26
+ start_datum=data["startDatum"],
27
+ actief=data["actief"],
28
+ )
29
+
30
+
31
+ @dataclass
32
+ class MutatieAbonnementenAPI:
33
+ klant_id: str
34
+ abonnementen: list[Abonnement]
35
+
36
+ @staticmethod
37
+ def from_dict(data: dict) -> "MutatieAbonnementenAPI": # noqa: D102
38
+ return MutatieAbonnementenAPI(
39
+ klant_id=data["klantId"], abonnementen=[Abonnement.from_dict(a) for a in data["abonnementen"]]
40
+ )