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.
- kvk_connect/__init__.py +11 -0
- kvk_connect/api/__init__.py +4 -0
- kvk_connect/api/client.py +183 -0
- kvk_connect/api/endpoints.py +24 -0
- kvk_connect/api/session.py +34 -0
- kvk_connect/cli/main.py +26 -0
- kvk_connect/db/__init__.py +0 -0
- kvk_connect/db/basisprofiel_reader.py +67 -0
- kvk_connect/db/basisprofiel_writer.py +73 -0
- kvk_connect/db/init.py +25 -0
- kvk_connect/db/kvkvestigingen_reader.py +41 -0
- kvk_connect/db/kvkvestigingen_writer.py +73 -0
- kvk_connect/db/signaal_reader.py +23 -0
- kvk_connect/db/signaal_writer.py +73 -0
- kvk_connect/db/vestigingenprofiel_reader.py +66 -0
- kvk_connect/db/vestigingsprofiel_writer.py +92 -0
- kvk_connect/logging_config.py +27 -0
- kvk_connect/mappers/__init__.py +1 -0
- kvk_connect/mappers/kvk_record_mapper.py +100 -0
- kvk_connect/mappers/map_mutatie_abonnement_api_to_mutatieabonnement.py +11 -0
- kvk_connect/mappers/map_vestigingen_api_to_vestigingsnummers.py +14 -0
- kvk_connect/mappers/map_vestigingsprofiel_api_to_vestigingsprofiel_domain.py +41 -0
- kvk_connect/models/__init__.py +0 -0
- kvk_connect/models/api/__init__.py +0 -0
- kvk_connect/models/api/abonnementen_api.py +42 -0
- kvk_connect/models/api/basisprofiel_api.py +233 -0
- kvk_connect/models/api/mutatie_abonnementen_api.py +40 -0
- kvk_connect/models/api/mutatiesignalen_api.py +44 -0
- kvk_connect/models/api/vestigingen_api.py +73 -0
- kvk_connect/models/api/vestigingsprofiel_api.py +71 -0
- kvk_connect/models/domain/__init__.py +6 -0
- kvk_connect/models/domain/basisprofiel.py +65 -0
- kvk_connect/models/domain/kvkvestigingsnummersdomain.py +28 -0
- kvk_connect/models/domain/mutatie_abonnement.py +20 -0
- kvk_connect/models/domain/vestigingsadresdomain.py +62 -0
- kvk_connect/models/domain/vestigingsadressendomain.py +48 -0
- kvk_connect/models/domain/vestigingsprofiel_domain.py +58 -0
- kvk_connect/models/orm/base.py +5 -0
- kvk_connect/models/orm/basisprofiel_orm.py +52 -0
- kvk_connect/models/orm/kvkvestigingen_orm.py +53 -0
- kvk_connect/models/orm/signaal_orm.py +40 -0
- kvk_connect/models/orm/vestigingsprofiel_orm.py +58 -0
- kvk_connect/services/__init__.py +4 -0
- kvk_connect/services/record_service.py +66 -0
- kvk_connect/utils/__init__.py +5 -0
- kvk_connect/utils/env.py +16 -0
- kvk_connect/utils/formatting.py +11 -0
- kvk_connect/utils/rate_limit.py +21 -0
- kvk_connect/utils/tools.py +131 -0
- kvk_connect-0.1.6.dist-info/METADATA +352 -0
- kvk_connect-0.1.6.dist-info/RECORD +52 -0
- 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,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)
|
kvk_connect/utils/env.py
ADDED
|
@@ -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)
|