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,58 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import UTC, datetime
4
+
5
+ from sqlalchemy import Date, DateTime, Float, Index, Integer, String
6
+ from sqlalchemy.orm import Mapped, mapped_column
7
+
8
+ from kvk_connect.models.orm.base import Base
9
+
10
+
11
+ class VestigingsProfielORM(Base):
12
+ __tablename__ = "vestigingsprofielen"
13
+
14
+ # Primary key
15
+ vestigingsnummer: Mapped[str] = mapped_column("vestigingsnummer", String(12), primary_key=True, index=True)
16
+
17
+ # Correspondentie adres velden
18
+ cor_adres_volledig: Mapped[str | None] = mapped_column("corAdresVolledig", String(500))
19
+ cor_adres_postcode: Mapped[str | None] = mapped_column("corAdresPostcode", String(16))
20
+ cor_adres_postbusnummer: Mapped[int | None] = mapped_column("corAdresPostbusnummer", Integer)
21
+ cor_adres_plaats: Mapped[str | None] = mapped_column("corAdresPlaats", String(255))
22
+ cor_adres_land: Mapped[str | None] = mapped_column("corAdresLand", String(100))
23
+
24
+ # Bezoek adres velden
25
+ bzk_adres_volledig: Mapped[str | None] = mapped_column("bzkAdresVolledig", String(500))
26
+ bzk_adres_straatnaam: Mapped[str | None] = mapped_column("bzkAdresStraatnaam", String(255))
27
+ bzk_adres_huisnummer: Mapped[int | None] = mapped_column("bzkAdresHuisnummer", Integer)
28
+ bzk_adres_postcode: Mapped[str | None] = mapped_column("bzkAdresPostcode", String(16))
29
+ bzk_adres_plaats: Mapped[str | None] = mapped_column("bzkAdresPlaats", String(255))
30
+ bzk_adres_land: Mapped[str | None] = mapped_column("bzkAdresLand", String(100))
31
+
32
+ # GPS coördinaten
33
+ bzk_adres_gps_latitude: Mapped[float | None] = mapped_column("bzkAdresGpsLatitude", Float)
34
+ bzk_adres_gps_longitude: Mapped[float | None] = mapped_column("bzkAdresGpsLongitude", Float)
35
+
36
+ # Registratie datums
37
+ registratie_datum_aanvang_vestiging: Mapped[datetime | None] = mapped_column(
38
+ "RegistratieDatumAanvangVestiging", Date
39
+ )
40
+ registratie_datum_einde_vestiging: Mapped[datetime | None] = mapped_column("RegistratieDatumEindeVestiging", Date)
41
+
42
+ # Timestamp velden met defaults
43
+ created_at: Mapped[datetime] = mapped_column(
44
+ "created_at", DateTime(timezone=True), default=lambda: datetime.now(UTC)
45
+ )
46
+
47
+ last_updated: Mapped[datetime] = mapped_column(
48
+ "last_updated",
49
+ DateTime(timezone=True),
50
+ default=lambda: datetime.now(UTC),
51
+ onupdate=lambda: datetime.now(UTC),
52
+ index=True, # Index voor last_updated filtering
53
+ )
54
+
55
+ __table_args__ = (
56
+ # Index voor joins met kvkvestigingen
57
+ Index("ix_vestigingsprofiel_vest_updated", vestigingsnummer, last_updated),
58
+ )
@@ -0,0 +1,4 @@
1
+ # mappers package initialization
2
+ from .record_service import KVKRecordService
3
+
4
+ __all__ = ["KVKRecordService"]
@@ -0,0 +1,66 @@
1
+ import logging
2
+
3
+ from kvk_connect.api.client import KVKApiClient
4
+ from kvk_connect.mappers.kvk_record_mapper import map_kvkbasisprofiel_api_to_kvkrecord
5
+ from kvk_connect.mappers.map_vestigingen_api_to_vestigingsnummers import map_vestigingen_api_to_vestigingsnummers
6
+ from kvk_connect.mappers.map_vestigingsprofiel_api_to_vestigingsprofiel_domain import (
7
+ map_vestigingsprofiel_api_to_vestigingsprofiel_domain,
8
+ )
9
+ from kvk_connect.models.domain import KvKVestigingsNummersDomain
10
+ from kvk_connect.models.domain.basisprofiel import BasisProfielDomain
11
+ from kvk_connect.models.domain.vestigingsprofiel_domain import VestigingsProfielDomain
12
+ from kvk_connect.utils.tools import clean_and_pad
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class KVKRecordService:
18
+ """Service for fetching and mapping KVK records to domain models."""
19
+
20
+ def __init__(self, client: KVKApiClient) -> None:
21
+ self.client = client
22
+
23
+ def get_basisprofiel(self, kvk_nummer: str) -> BasisProfielDomain | None:
24
+ """Fetch and map basisprofiel for a given KVK number.
25
+
26
+ Returns:
27
+ BasisProfielDomain if found, None otherwise.
28
+ """
29
+ kvk_nummer = clean_and_pad(kvk_nummer)
30
+ bp_api = self.client.get_basisinformatie(kvk_nummer)
31
+
32
+ if bp_api is None:
33
+ logger.info("No basisprofiel found for KVK number %s", kvk_nummer)
34
+ return None
35
+
36
+ return map_kvkbasisprofiel_api_to_kvkrecord(bp_api)
37
+
38
+ def get_vestigingen(self, kvk_nummer: str) -> KvKVestigingsNummersDomain | None:
39
+ """Fetch and map vestigingen for a given KVK number.
40
+
41
+ Returns:
42
+ KvKVestigingsNummersDomain if found, None otherwise.
43
+ """
44
+ kvk_nummer = clean_and_pad(kvk_nummer)
45
+ vn_api = self.client.get_vestigingen(kvk_nummer)
46
+
47
+ if vn_api is None:
48
+ logger.info("No vestigingen found for KVK number %s", kvk_nummer)
49
+ return None
50
+
51
+ return map_vestigingen_api_to_vestigingsnummers(vn_api)
52
+
53
+ def get_vestigingsprofiel(self, vestigings_nummer: str) -> VestigingsProfielDomain | None:
54
+ """Fetch and map vestigingsprofiel for a given vestigingsnummer.
55
+
56
+ Returns:
57
+ VestigingsProfielDomain if found, None otherwise.
58
+ """
59
+ vestigings_nummer = clean_and_pad(vestigings_nummer)
60
+ vp_api = self.client.get_vestigingsprofiel(vestigings_nummer, geo_data=True)
61
+
62
+ if vp_api is None:
63
+ logger.info("No vestigingsprofiel found for vestigingsnummer %s", vestigings_nummer)
64
+ return None
65
+
66
+ return map_vestigingsprofiel_api_to_vestigingsprofiel_domain(vp_api)
@@ -0,0 +1,5 @@
1
+ from .env import get_env
2
+ from .formatting import truncate_float
3
+ from .rate_limit import global_rate_limit
4
+
5
+ __all__ = ["get_env", "truncate_float", "global_rate_limit"]
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ from dotenv import load_dotenv
6
+
7
+ load_dotenv()
8
+
9
+
10
+ def get_env(name: str, default: str | None = None, required: bool = False) -> str | None:
11
+ """Haal een omgevingsvariabele op."""
12
+
13
+ value = os.getenv(name, default)
14
+ if required and value is None:
15
+ raise RuntimeError(f"Missing required environment variable: {name}")
16
+ return value
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ def truncate_float(value: float | None, digits: int = 5) -> str:
5
+ """Truncate a float to a given number of digits after the decimal point and return as string."""
6
+
7
+ if value is None or value == 0.0:
8
+ return ""
9
+ factor = 10**digits
10
+ truncated = int(value * factor) / factor
11
+ return f"{truncated:.{digits}f}".replace(".", ",")
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ from ratelimit import limits, sleep_and_retry
6
+
7
+ RATE_LIMIT_CALLS = int(os.getenv("RATE_LIMIT_CALLS", "100"))
8
+
9
+
10
+ def global_rate_limit(calls: int = RATE_LIMIT_CALLS, period: int = 1):
11
+ """Decorator to apply a global rate limit to a function."""
12
+
13
+ def deco(func):
14
+ @sleep_and_retry
15
+ @limits(calls=calls, period=period)
16
+ def wrapper(*args, **kwargs):
17
+ return func(*args, **kwargs)
18
+
19
+ return wrapper
20
+
21
+ return deco
@@ -0,0 +1,131 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ import re
6
+ from datetime import UTC, date, datetime, timedelta
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ def parse_kvk_datum(datum_str: str | None) -> date | None:
12
+ """Parse een KVK datum string naar een date object.
13
+
14
+ Ondersteunt:
15
+ - DD-MM-YYYY (standaard KVK formaat)
16
+ - YYYYMMDD (8 cijfers)
17
+ - YYYYMM00 (6 cijfers + 00, zet dag op 1)
18
+ - YYYY0000 (4 cijfers + 0000, zet maand en dag op 1)
19
+
20
+ Args:
21
+ datum_str: Datum string of None
22
+
23
+ Returns:
24
+ date object of None bij ongeldige/lege input
25
+ """
26
+ if datum_str is None or str(datum_str).strip() in ("", "None"):
27
+ return None
28
+
29
+ datum_str = str(datum_str).strip()
30
+
31
+ # Probeer standaard DD-MM-YYYY formaat
32
+ try:
33
+ return datetime.strptime(datum_str, "%d-%m-%Y").date()
34
+ except ValueError:
35
+ pass
36
+
37
+ # Probeer YYYYMMDD formaat (8 cijfers)
38
+ if datum_str.isdigit() and len(datum_str) == 8:
39
+ try:
40
+ year = int(datum_str[0:4])
41
+ month = int(datum_str[4:6])
42
+ day = int(datum_str[6:8])
43
+
44
+ # Als maand of dag 0 is, zet op 1
45
+ if month == 0:
46
+ month = 1
47
+ if day == 0:
48
+ day = 1
49
+
50
+ return date(year, month, day)
51
+ except (ValueError, OverflowError) as e:
52
+ logger.warning("Ongeldige datum conversie voor '%s': {%s}", datum_str, e)
53
+ return None
54
+
55
+ logger.warning("Ongeldige datum conversie voor '%s': geen geldig formaat", datum_str)
56
+ return None
57
+
58
+
59
+ def truncate_float(value: float | None, digits: int = 5) -> str:
60
+ """Truncate a float to a given number of digits after the decimal point and return as string.
61
+
62
+ If value is None, return empty string.
63
+ """
64
+ if value is None or value == 0.0:
65
+ return ""
66
+ factor = 10**digits
67
+ truncated = int(value * factor) / factor
68
+ # Format with fixed number of digits en gebruik komma als decimaalteken
69
+ return f"{truncated:.{digits}f}".replace(".", ",")
70
+
71
+
72
+ def clean_and_pad(s, fill=8):
73
+ """Strip non-digit characters from start/end and pad to given length with leading zeros."""
74
+
75
+ cleaned = re.sub(r"[^\d]", "", s)
76
+ # Pad to 8 digits
77
+ return cleaned.zfill(fill)
78
+
79
+
80
+ def formatteer_datum(datum_str):
81
+ """Formatteer een datum string van YYYYMMDD naar DD-MM-YYYY. Retourneer None bij "None"."""
82
+
83
+ if datum_str != "None":
84
+ try:
85
+ return datetime.strptime(datum_str, "%Y%m%d").strftime("%d-%m-%Y")
86
+ except ValueError:
87
+ return datum_str
88
+ else:
89
+ return None
90
+
91
+
92
+ def print_response(_response):
93
+ """Prints the response from a requests call in a formatted way. Handles JSON and non-JSON responses."""
94
+
95
+ print("url:", _response.url)
96
+ print("Status code:", _response.status_code)
97
+ try:
98
+ data = _response.json()
99
+ print("Response data (JSON):")
100
+ print(json.dumps(data, indent=4))
101
+ except ValueError:
102
+ print("Response data (text):")
103
+ print(_response.text)
104
+
105
+
106
+ def get_timeselector(selected_from: datetime, selected_to: datetime) -> list[dict[str, datetime]]:
107
+ """Gegeven twee datetime timestamps.Returned een lijst met from-to dicts terug van maximaal een week.
108
+
109
+ Wordt gebruikt om pagination van mutaties op te halen deze mogen maximaal per week opgehaald worden
110
+ """
111
+
112
+ def to_utc(dt: datetime) -> datetime:
113
+ return dt.astimezone(UTC) if dt.tzinfo else dt.replace(tzinfo=UTC)
114
+
115
+ def split_into_chunks(start: datetime, end: datetime, days: int = 7) -> list[dict[str, datetime]]:
116
+ out: list[dict[str, datetime]] = []
117
+ cur = start
118
+ step = timedelta(days=days)
119
+ while cur < end:
120
+ nxt = min(cur + step, end)
121
+ out.append({"from": cur, "to": nxt})
122
+ cur = nxt
123
+ return out
124
+
125
+ # Normalize selection to UTC and ensure ascending order
126
+ sf, st = to_utc(selected_from), to_utc(selected_to)
127
+ if st < sf:
128
+ logger.warning("Selected time from is farther into the future than the selected time to.")
129
+ return []
130
+ else:
131
+ return split_into_chunks(sf, st)