parkapi-sources 0.2.0__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.
- parkapi_sources/__init__.py +6 -0
- parkapi_sources/_version.py +16 -0
- parkapi_sources/converters/__init__.py +19 -0
- parkapi_sources/converters/bahn_v2/__init__.py +6 -0
- parkapi_sources/converters/bahn_v2/converter.py +72 -0
- parkapi_sources/converters/bahn_v2/mapper.py +38 -0
- parkapi_sources/converters/bahn_v2/validators.py +131 -0
- parkapi_sources/converters/base_converter/__init__.py +6 -0
- parkapi_sources/converters/base_converter/base_converter.py +26 -0
- parkapi_sources/converters/base_converter/pull/__init__.py +8 -0
- parkapi_sources/converters/base_converter/pull/pull_converter.py +20 -0
- parkapi_sources/converters/base_converter/pull/pull_scraper_mixin.py +74 -0
- parkapi_sources/converters/base_converter/pull/static_geojson_data_mixin/__init__.py +7 -0
- parkapi_sources/converters/base_converter/pull/static_geojson_data_mixin/mixin.py +69 -0
- parkapi_sources/converters/base_converter/pull/static_geojson_data_mixin/models.py +63 -0
- parkapi_sources/converters/base_converter/push/__init__.py +12 -0
- parkapi_sources/converters/base_converter/push/csv_converter.py +39 -0
- parkapi_sources/converters/base_converter/push/json_converter.py +19 -0
- parkapi_sources/converters/base_converter/push/normalized_xlsx_converter.py +105 -0
- parkapi_sources/converters/base_converter/push/push_converter.py +12 -0
- parkapi_sources/converters/base_converter/push/xlsx_converter.py +47 -0
- parkapi_sources/converters/base_converter/push/xml_converter.py +21 -0
- parkapi_sources/converters/bfrk_bw/__init__.py +7 -0
- parkapi_sources/converters/bfrk_bw/base_converter.py +56 -0
- parkapi_sources/converters/bfrk_bw/base_models.py +57 -0
- parkapi_sources/converters/bfrk_bw/bike_converter.py +50 -0
- parkapi_sources/converters/bfrk_bw/bike_models.py +56 -0
- parkapi_sources/converters/bfrk_bw/car_converter.py +49 -0
- parkapi_sources/converters/bfrk_bw/car_models.py +46 -0
- parkapi_sources/converters/freiburg/__init__.py +6 -0
- parkapi_sources/converters/freiburg/converter.py +65 -0
- parkapi_sources/converters/freiburg/models.py +43 -0
- parkapi_sources/converters/heidelberg/__init__.py +6 -0
- parkapi_sources/converters/heidelberg/converter.py +74 -0
- parkapi_sources/converters/heidelberg/validators.py +56 -0
- parkapi_sources/converters/karlsruhe/__init__.py +6 -0
- parkapi_sources/converters/karlsruhe/converter.py +89 -0
- parkapi_sources/converters/karlsruhe/files/ca.crt.pem +49 -0
- parkapi_sources/converters/karlsruhe/models.py +103 -0
- parkapi_sources/converters/mannheim/__init__.py +6 -0
- parkapi_sources/converters/mannheim/converter.py +54 -0
- parkapi_sources/converters/neckarsulm/__init__.py +6 -0
- parkapi_sources/converters/neckarsulm/converter.py +70 -0
- parkapi_sources/converters/neckarsulm/models.py +71 -0
- parkapi_sources/converters/pbw/__init__.py +6 -0
- parkapi_sources/converters/pbw/converter.py +117 -0
- parkapi_sources/converters/pbw/mapper.py +53 -0
- parkapi_sources/converters/pbw/validation.py +279 -0
- parkapi_sources/converters/pforzheim/__init__.py +6 -0
- parkapi_sources/converters/pforzheim/converter.py +62 -0
- parkapi_sources/converters/pforzheim/validation.py +43 -0
- parkapi_sources/converters/reutlingen/__init__.py +6 -0
- parkapi_sources/converters/reutlingen/converter.py +73 -0
- parkapi_sources/converters/reutlingen/validation.py +53 -0
- parkapi_sources/converters/stuttgart/__init__.py +6 -0
- parkapi_sources/converters/stuttgart/converter.py +132 -0
- parkapi_sources/converters/ulm/__init__.py +6 -0
- parkapi_sources/converters/ulm/converter.py +52 -0
- parkapi_sources/converters/vrs_p_r/__init__.py +6 -0
- parkapi_sources/converters/vrs_p_r/vrs_p_r.py +56 -0
- parkapi_sources/exceptions.py +47 -0
- parkapi_sources/models/__init__.py +8 -0
- parkapi_sources/models/enums.py +65 -0
- parkapi_sources/models/parking_site_inputs.py +136 -0
- parkapi_sources/models/source_info.py +23 -0
- parkapi_sources/models/xlsx_inputs.py +80 -0
- parkapi_sources/parkapi_sources.py +88 -0
- parkapi_sources/scripts/parkapi.py +210 -0
- parkapi_sources/util/__init__.py +8 -0
- parkapi_sources/util/config_helper.py +16 -0
- parkapi_sources/util/encoding.py +37 -0
- parkapi_sources/util/xml_helper.py +279 -0
- parkapi_sources/validators/__init__.py +13 -0
- parkapi_sources/validators/boolean_validators.py +24 -0
- parkapi_sources/validators/date_validator.py +26 -0
- parkapi_sources/validators/datetime_validator.py +32 -0
- parkapi_sources/validators/decimal_validators.py +18 -0
- parkapi_sources/validators/integer_validators.py +42 -0
- parkapi_sources/validators/noneable.py +23 -0
- parkapi_sources/validators/string_validators.py +32 -0
- parkapi_sources/validators/time_validators.py +17 -0
- parkapi_sources-0.2.0.dist-info/LICENCE.txt +21 -0
- parkapi_sources-0.2.0.dist-info/METADATA +256 -0
- parkapi_sources-0.2.0.dist-info/RECORD +86 -0
- parkapi_sources-0.2.0.dist-info/WHEEL +5 -0
- parkapi_sources-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# file generated by setuptools_scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
TYPE_CHECKING = False
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from typing import Tuple, Union
|
|
6
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
7
|
+
else:
|
|
8
|
+
VERSION_TUPLE = object
|
|
9
|
+
|
|
10
|
+
version: str
|
|
11
|
+
__version__: str
|
|
12
|
+
__version_tuple__: VERSION_TUPLE
|
|
13
|
+
version_tuple: VERSION_TUPLE
|
|
14
|
+
|
|
15
|
+
__version__ = version = '0.2.0'
|
|
16
|
+
__version_tuple__ = version_tuple = (0, 2, 0)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2024 binary butterfly GmbH
|
|
3
|
+
Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .bahn_v2 import BahnV2PullConverter
|
|
7
|
+
from .base_converter import BaseConverter
|
|
8
|
+
from .bfrk_bw import BfrkBwOepnvBikePushConverter, BfrkBwOepnvCarPushConverter, BfrkBwSpnvBikePushConverter, BfrkBwSpnvCarPushConverter
|
|
9
|
+
from .freiburg import FreiburgPullConverter
|
|
10
|
+
from .heidelberg import HeidelbergPullConverter
|
|
11
|
+
from .karlsruhe import KarlsruhePullConverter
|
|
12
|
+
from .mannheim import MannheimPullConverter
|
|
13
|
+
from .neckarsulm import NeckarsulmPushConverter
|
|
14
|
+
from .pbw import PbwPullConverter
|
|
15
|
+
from .pforzheim import PforzheimPushConverter
|
|
16
|
+
from .reutlingen import ReutlingenPushConverter
|
|
17
|
+
from .stuttgart import StuttgartPushConverter
|
|
18
|
+
from .ulm import UlmPullConverter
|
|
19
|
+
from .vrs_p_r import VrsParkAndRidePushConverter
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2024 binary butterfly GmbH
|
|
3
|
+
Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
from validataclass.exceptions import ValidationError
|
|
8
|
+
from validataclass.validators import DataclassValidator
|
|
9
|
+
|
|
10
|
+
from parkapi_sources.converters.base_converter.pull import PullConverter
|
|
11
|
+
from parkapi_sources.exceptions import ImportParkingSiteException
|
|
12
|
+
from parkapi_sources.models import RealtimeParkingSiteInput, SourceInfo, StaticParkingSiteInput
|
|
13
|
+
|
|
14
|
+
from .mapper import BahnMapper
|
|
15
|
+
from .validators import BahnParkingSiteInput
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BahnV2PullConverter(PullConverter):
|
|
19
|
+
_base_url = 'https://apis.deutschebahn.com/db-api-marketplace/apis/parking-information/db-bahnpark/v2'
|
|
20
|
+
required_config_keys = ['PARK_API_BAHN_API_CLIENT_ID', 'PARK_API_BAHN_API_CLIENT_SECRET']
|
|
21
|
+
|
|
22
|
+
mapper = BahnMapper()
|
|
23
|
+
bahn_parking_site_validator = DataclassValidator(BahnParkingSiteInput)
|
|
24
|
+
|
|
25
|
+
source_info = SourceInfo(
|
|
26
|
+
uid='bahn_v2',
|
|
27
|
+
name='Deutsche Bahn Parkplätze',
|
|
28
|
+
public_url='https://www.dbbahnpark.de',
|
|
29
|
+
has_realtime_data=False, # ATM it's impossible to get realtime data due rate limit restrictions
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def get_static_parking_sites(self) -> tuple[list[StaticParkingSiteInput], list[ImportParkingSiteException]]:
|
|
33
|
+
static_parking_site_inputs: list = []
|
|
34
|
+
static_parking_site_errors: list[ImportParkingSiteException] = []
|
|
35
|
+
|
|
36
|
+
parking_site_dicts = self.get_data()
|
|
37
|
+
|
|
38
|
+
for parking_site_dict in parking_site_dicts.get('_embedded', []):
|
|
39
|
+
try:
|
|
40
|
+
parking_site_input: BahnParkingSiteInput = self.bahn_parking_site_validator.validate(parking_site_dict)
|
|
41
|
+
except ValidationError as e:
|
|
42
|
+
static_parking_site_errors.append(
|
|
43
|
+
ImportParkingSiteException(
|
|
44
|
+
source_uid=self.source_info.uid,
|
|
45
|
+
parking_site_uid=parking_site_dict.get('id'),
|
|
46
|
+
message=f'validation error for data {parking_site_dict}: {e.to_dict()}',
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
static_parking_site_inputs.append(
|
|
52
|
+
self.mapper.map_static_parking_site(parking_site_input),
|
|
53
|
+
)
|
|
54
|
+
return static_parking_site_inputs, static_parking_site_errors
|
|
55
|
+
|
|
56
|
+
def get_realtime_parking_sites(self) -> tuple[list[RealtimeParkingSiteInput], list[ImportParkingSiteException]]:
|
|
57
|
+
return [], [] # ATM it's impossible to get realtime data due rate limit restrictions
|
|
58
|
+
|
|
59
|
+
def get_data(self) -> dict:
|
|
60
|
+
headers: dict[str, str] = {
|
|
61
|
+
'DB-Client-Id': self.config_helper.get('PARK_API_BAHN_API_CLIENT_ID'),
|
|
62
|
+
'DB-Api-Key': self.config_helper.get('PARK_API_BAHN_API_CLIENT_SECRET'),
|
|
63
|
+
'Accept': 'application/vnd.parkinginformation.db-bahnpark.v1+json',
|
|
64
|
+
'accept': 'application/json',
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
response = requests.get(
|
|
68
|
+
f'{self.config_helper.get("PARK_API_BAHN_URL", self._base_url)}/parking-facilities',
|
|
69
|
+
headers=headers,
|
|
70
|
+
timeout=60,
|
|
71
|
+
)
|
|
72
|
+
return response.json()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2024 binary butterfly GmbH
|
|
3
|
+
Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
|
|
8
|
+
from parkapi_sources.models import StaticParkingSiteInput
|
|
9
|
+
|
|
10
|
+
from .validators import BahnParkingSiteCapacityType, BahnParkingSiteInput, NameContext
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BahnMapper:
|
|
14
|
+
@staticmethod
|
|
15
|
+
def map_static_parking_site(bahn_input: BahnParkingSiteInput) -> StaticParkingSiteInput:
|
|
16
|
+
static_parking_site_input = StaticParkingSiteInput(
|
|
17
|
+
uid=str(bahn_input.id),
|
|
18
|
+
name=next(iter(name_input.name for name_input in bahn_input.name if name_input.context == NameContext.NAME)),
|
|
19
|
+
lat=bahn_input.address.location.latitude,
|
|
20
|
+
lon=bahn_input.address.location.longitude,
|
|
21
|
+
operator_name=bahn_input.operator.name,
|
|
22
|
+
address=f'{bahn_input.address.streetAndNumber}, {bahn_input.address.zip} {bahn_input.address.city}',
|
|
23
|
+
capacity=bahn_input.capacity,
|
|
24
|
+
type=bahn_input.type.name.to_parking_site_type_input(),
|
|
25
|
+
has_realtime_data=False, # TODO: change this as soon as Bahn offers proper rate limits
|
|
26
|
+
static_data_updated_at=datetime.now(tz=timezone.utc),
|
|
27
|
+
public_url=bahn_input.url,
|
|
28
|
+
)
|
|
29
|
+
if bahn_input.access.openingHours.is24h:
|
|
30
|
+
static_parking_site_input.opening_hours = '24/7'
|
|
31
|
+
|
|
32
|
+
for capacity_data in bahn_input.capacity:
|
|
33
|
+
if capacity_data.type == BahnParkingSiteCapacityType.PARKING:
|
|
34
|
+
static_parking_site_input.capacity = int(round(capacity_data.total))
|
|
35
|
+
elif capacity_data.type == BahnParkingSiteCapacityType.HANDICAPPED_PARKING:
|
|
36
|
+
static_parking_site_input.capacity_disabled = int(round(capacity_data.total))
|
|
37
|
+
|
|
38
|
+
return static_parking_site_input
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2024 binary butterfly GmbH
|
|
3
|
+
Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from validataclass.dataclasses import Default, validataclass
|
|
11
|
+
from validataclass.validators import (
|
|
12
|
+
BooleanValidator,
|
|
13
|
+
DataclassValidator,
|
|
14
|
+
DecimalValidator,
|
|
15
|
+
EnumValidator,
|
|
16
|
+
IntegerValidator,
|
|
17
|
+
ListValidator,
|
|
18
|
+
Noneable,
|
|
19
|
+
NumericValidator,
|
|
20
|
+
StringValidator,
|
|
21
|
+
UrlValidator,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
from parkapi_sources.models.enums import ParkingSiteType
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class NameContext(Enum):
|
|
28
|
+
NAME = 'NAME'
|
|
29
|
+
DISPLAY = 'DISPLAY'
|
|
30
|
+
LABEL = 'LABEL'
|
|
31
|
+
SLOGAN = 'SLOGAN'
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class BahnParkingSiteCapacityType(Enum):
|
|
35
|
+
PARKING = 'PARKING'
|
|
36
|
+
HANDICAPPED_PARKING = 'HANDICAPPED_PARKING'
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class BahnParkingSiteType(Enum):
|
|
40
|
+
PARKPLATZ = 'Parkplatz'
|
|
41
|
+
TIEFGARAGE = 'Tiefgarage'
|
|
42
|
+
PARKHAUS = 'Parkhaus'
|
|
43
|
+
STRASSE = 'Straße'
|
|
44
|
+
PARKDECK = 'Parkdeck'
|
|
45
|
+
|
|
46
|
+
def to_parking_site_type_input(self) -> ParkingSiteType:
|
|
47
|
+
# TODO: find out more details about this enumeration for a proper mapping
|
|
48
|
+
return {
|
|
49
|
+
self.PARKPLATZ: ParkingSiteType.ON_STREET,
|
|
50
|
+
self.PARKHAUS: ParkingSiteType.CAR_PARK,
|
|
51
|
+
self.TIEFGARAGE: ParkingSiteType.UNDERGROUND,
|
|
52
|
+
self.STRASSE: ParkingSiteType.ON_STREET,
|
|
53
|
+
self.PARKDECK: ParkingSiteType.CAR_PARK,
|
|
54
|
+
}.get(self, ParkingSiteType.OTHER)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@validataclass
|
|
58
|
+
class BahnNameInput:
|
|
59
|
+
name: str = StringValidator()
|
|
60
|
+
context: NameContext = EnumValidator(NameContext)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@validataclass
|
|
64
|
+
class BahnTypeInput:
|
|
65
|
+
name: BahnParkingSiteType = EnumValidator(BahnParkingSiteType)
|
|
66
|
+
nameEn: str = StringValidator()
|
|
67
|
+
abbreviation: str = StringValidator()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@validataclass
|
|
71
|
+
class BahnOperatorInput:
|
|
72
|
+
name: str = StringValidator()
|
|
73
|
+
# url: str = UrlValidator() # TODO: urls are broken
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@validataclass
|
|
77
|
+
class BahnLocationInput:
|
|
78
|
+
longitude: Decimal = NumericValidator()
|
|
79
|
+
latitude: Decimal = NumericValidator()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@validataclass
|
|
83
|
+
class BahnAdressInput:
|
|
84
|
+
streetAndNumber: str = StringValidator()
|
|
85
|
+
zip: str = StringValidator()
|
|
86
|
+
city: str = StringValidator()
|
|
87
|
+
phone: Optional[str] = Noneable(StringValidator())
|
|
88
|
+
location: BahnLocationInput = DataclassValidator(BahnLocationInput)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@validataclass
|
|
92
|
+
class BahnCapacityInput:
|
|
93
|
+
type: BahnParkingSiteCapacityType = EnumValidator(BahnParkingSiteCapacityType)
|
|
94
|
+
total: int = IntegerValidator(allow_strings=True, min_value=0)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@validataclass
|
|
98
|
+
class BahnOpeningHoursInput:
|
|
99
|
+
text: Optional[str] = StringValidator(), Default(None)
|
|
100
|
+
is24h: bool = BooleanValidator()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@validataclass
|
|
104
|
+
class BahnClearanceInput:
|
|
105
|
+
height: Optional[Decimal] = Noneable(DecimalValidator()), Default(None)
|
|
106
|
+
width: Optional[Decimal] = Noneable(DecimalValidator()), Default(None)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@validataclass
|
|
110
|
+
class BahnRestrictionInput:
|
|
111
|
+
clearance: BahnClearanceInput = DataclassValidator(BahnClearanceInput)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@validataclass
|
|
115
|
+
class BahnAccessInput:
|
|
116
|
+
openingHours: BahnOpeningHoursInput = DataclassValidator(BahnOpeningHoursInput)
|
|
117
|
+
restrictions: BahnRestrictionInput = DataclassValidator(BahnRestrictionInput)
|
|
118
|
+
# TODO: ignored multible attributes which do not matter so far
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@validataclass
|
|
122
|
+
class BahnParkingSiteInput:
|
|
123
|
+
id: int = IntegerValidator(allow_strings=True)
|
|
124
|
+
name: list[BahnNameInput] = ListValidator(DataclassValidator(BahnNameInput))
|
|
125
|
+
url: Optional[str] = UrlValidator(), Default(None)
|
|
126
|
+
type: BahnTypeInput = DataclassValidator(BahnTypeInput)
|
|
127
|
+
operator: BahnOperatorInput = DataclassValidator(BahnOperatorInput)
|
|
128
|
+
address: BahnAdressInput = DataclassValidator(BahnAdressInput)
|
|
129
|
+
capacity: list[BahnCapacityInput] = ListValidator(DataclassValidator(BahnCapacityInput))
|
|
130
|
+
access: BahnAccessInput = DataclassValidator(BahnAccessInput)
|
|
131
|
+
# TODO: ignored multible attributes which do not matter so far
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2023 binary butterfly GmbH
|
|
3
|
+
Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
|
|
8
|
+
from validataclass.validators import DataclassValidator
|
|
9
|
+
|
|
10
|
+
from parkapi_sources.models import RealtimeParkingSiteInput, SourceInfo, StaticParkingSiteInput
|
|
11
|
+
from parkapi_sources.util import ConfigHelper
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaseConverter(ABC):
|
|
15
|
+
config_helper: ConfigHelper
|
|
16
|
+
static_parking_site_validator = DataclassValidator(StaticParkingSiteInput)
|
|
17
|
+
realtime_parking_site_validator = DataclassValidator(RealtimeParkingSiteInput)
|
|
18
|
+
required_config_keys: list[str] = []
|
|
19
|
+
|
|
20
|
+
def __init__(self, config_helper: ConfigHelper):
|
|
21
|
+
self.config_helper = config_helper
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def source_info(self) -> SourceInfo:
|
|
26
|
+
pass
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2024 binary butterfly GmbH
|
|
3
|
+
Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .pull_converter import PullConverter
|
|
7
|
+
from .pull_scraper_mixin import PullScraperMixin
|
|
8
|
+
from .static_geojson_data_mixin import GeojsonFeatureGeometryInput, GeojsonInput, StaticGeojsonDataMixin
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2023 binary butterfly GmbH
|
|
3
|
+
Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from abc import abstractmethod
|
|
7
|
+
|
|
8
|
+
from parkapi_sources.converters.base_converter import BaseConverter
|
|
9
|
+
from parkapi_sources.exceptions import ImportParkingSiteException
|
|
10
|
+
from parkapi_sources.models import RealtimeParkingSiteInput, StaticParkingSiteInput
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PullConverter(BaseConverter):
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def get_static_parking_sites(self) -> tuple[list[StaticParkingSiteInput], list[ImportParkingSiteException]]:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def get_realtime_parking_sites(self) -> tuple[list[RealtimeParkingSiteInput], list[ImportParkingSiteException]]:
|
|
20
|
+
pass
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2024 binary butterfly GmbH
|
|
3
|
+
Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
from bs4 import BeautifulSoup
|
|
11
|
+
from bs4.element import Tag
|
|
12
|
+
from validataclass.exceptions import ValidationError
|
|
13
|
+
from validataclass.validators import DataclassValidator
|
|
14
|
+
|
|
15
|
+
from parkapi_sources.exceptions import ImportParkingSiteException
|
|
16
|
+
from parkapi_sources.models import RealtimeParkingSiteInput, SourceInfo
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PullScraperMixin(ABC):
|
|
20
|
+
source_info: SourceInfo
|
|
21
|
+
realtime_parking_site_validator: DataclassValidator
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def get_realtime_tags_and_params(self) -> tuple[list[Tag], dict]:
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def realtime_tag_to_dict(self, tag: Tag, **kwargs) -> Optional[dict]:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
def load_url_in_soup(self, url: Optional[str] = None):
|
|
32
|
+
if url is None:
|
|
33
|
+
url = self.source_info.public_url
|
|
34
|
+
|
|
35
|
+
response = requests.get(url, timeout=30)
|
|
36
|
+
|
|
37
|
+
return BeautifulSoup(response.text, features='html.parser')
|
|
38
|
+
|
|
39
|
+
def _get_scraped_realtime_parking_site_inputs_and_exceptions(
|
|
40
|
+
self,
|
|
41
|
+
) -> tuple[list[RealtimeParkingSiteInput], list[ImportParkingSiteException]]:
|
|
42
|
+
realtime_parking_site_inputs: list[RealtimeParkingSiteInput] = []
|
|
43
|
+
import_parking_site_exceptions: list[ImportParkingSiteException] = []
|
|
44
|
+
|
|
45
|
+
tags, params = self.get_realtime_tags_and_params()
|
|
46
|
+
for tag in tags:
|
|
47
|
+
try:
|
|
48
|
+
realtime_parking_site_dict = self.realtime_tag_to_dict(tag, **params)
|
|
49
|
+
except Exception as e:
|
|
50
|
+
import_parking_site_exceptions.append(
|
|
51
|
+
ImportParkingSiteException(
|
|
52
|
+
source_uid=self.source_info.uid,
|
|
53
|
+
message=f'Invalid data: {e}',
|
|
54
|
+
),
|
|
55
|
+
)
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
# If realtime_parking_site_dict is None, we don't have a dataset to handle
|
|
59
|
+
if realtime_parking_site_dict is None:
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
realtime_parking_site_inputs.append(self.realtime_parking_site_validator.validate(realtime_parking_site_dict))
|
|
64
|
+
except ValidationError as e:
|
|
65
|
+
import_parking_site_exceptions.append(
|
|
66
|
+
ImportParkingSiteException(
|
|
67
|
+
source_uid=self.source_info.uid,
|
|
68
|
+
parking_site_uid=realtime_parking_site_dict.get('uid'),
|
|
69
|
+
message=f'Invallid data at uid {realtime_parking_site_dict.get("uid")}: {e.to_dict()}, '
|
|
70
|
+
f'data: {realtime_parking_site_dict}',
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return realtime_parking_site_inputs, import_parking_site_exceptions
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2024 binary butterfly GmbH
|
|
3
|
+
Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
from validataclass.exceptions import ValidationError
|
|
12
|
+
from validataclass.validators import DataclassValidator
|
|
13
|
+
|
|
14
|
+
from parkapi_sources.converters.base_converter.pull.static_geojson_data_mixin.models import GeojsonFeatureInput, GeojsonInput
|
|
15
|
+
from parkapi_sources.exceptions import ImportParkingSiteException, ImportSourceException
|
|
16
|
+
from parkapi_sources.models import SourceInfo, StaticParkingSiteInput
|
|
17
|
+
from parkapi_sources.util import ConfigHelper
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class StaticGeojsonDataMixin:
|
|
21
|
+
config_helper: ConfigHelper
|
|
22
|
+
source_info: SourceInfo
|
|
23
|
+
geojson_validator = DataclassValidator(GeojsonInput)
|
|
24
|
+
geojson_feature_validator = DataclassValidator(GeojsonFeatureInput)
|
|
25
|
+
_base_url = 'https://raw.githubusercontent.com/ParkenDD/parkapi-sources-v3/main/data'
|
|
26
|
+
|
|
27
|
+
def _get_static_geojson(self, source_uid: str) -> GeojsonInput:
|
|
28
|
+
if self.config_helper.get('STATIC_GEOJSON_BASE_PATH'):
|
|
29
|
+
with Path(self.config_helper.get('STATIC_GEOJSON_BASE_PATH'), f'{source_uid}.geojson').open() as geojson_file:
|
|
30
|
+
return json.loads(geojson_file.read())
|
|
31
|
+
else:
|
|
32
|
+
response = requests.get(f'{self.config_helper.get("STATIC_GEOJSON_BASE_URL")}/{source_uid}.geojson', timeout=30)
|
|
33
|
+
return response.json()
|
|
34
|
+
|
|
35
|
+
def _get_static_parking_site_inputs_and_exceptions(
|
|
36
|
+
self,
|
|
37
|
+
source_uid: str,
|
|
38
|
+
) -> tuple[list[StaticParkingSiteInput], list[ImportParkingSiteException]]:
|
|
39
|
+
geojson_dict = self._get_static_geojson(source_uid)
|
|
40
|
+
try:
|
|
41
|
+
geojson_input = self.geojson_validator.validate(geojson_dict)
|
|
42
|
+
except ValidationError as e:
|
|
43
|
+
raise ImportSourceException(
|
|
44
|
+
source_uid=source_uid,
|
|
45
|
+
message=f'Invalid GeoJSON for source {source_uid}: {e.to_dict()}. Data: {geojson_dict}',
|
|
46
|
+
) from e
|
|
47
|
+
|
|
48
|
+
static_parking_site_inputs: list[StaticParkingSiteInput] = []
|
|
49
|
+
import_parking_site_exceptions: list[ImportParkingSiteException] = []
|
|
50
|
+
|
|
51
|
+
for feature_dict in geojson_input.features:
|
|
52
|
+
try:
|
|
53
|
+
feature_input: GeojsonFeatureInput = self.geojson_feature_validator.validate(feature_dict)
|
|
54
|
+
static_parking_site_inputs.append(
|
|
55
|
+
feature_input.to_static_parking_site_input(
|
|
56
|
+
# TODO: Use the Last-Updated HTTP header instead, but as Github does not set such an header, we need to move
|
|
57
|
+
# all GeoJSON data in order to use this.
|
|
58
|
+
static_data_updated_at=datetime.now(tz=timezone.utc),
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
except ValidationError as e:
|
|
62
|
+
import_parking_site_exceptions.append(
|
|
63
|
+
ImportParkingSiteException(
|
|
64
|
+
source_uid=self.source_info.uid,
|
|
65
|
+
parking_site_uid=feature_dict.get('properties', {}).get('uid'),
|
|
66
|
+
message=f'Invalid GeoJSON feature for source {source_uid}: {e.to_dict()}',
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
return static_parking_site_inputs, import_parking_site_exceptions
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2024 binary butterfly GmbH
|
|
3
|
+
Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from decimal import Decimal
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from validataclass.dataclasses import Default, ValidataclassMixin, validataclass
|
|
11
|
+
from validataclass.validators import (
|
|
12
|
+
AnyOfValidator,
|
|
13
|
+
AnythingValidator,
|
|
14
|
+
BooleanValidator,
|
|
15
|
+
DataclassValidator,
|
|
16
|
+
EnumValidator,
|
|
17
|
+
IntegerValidator,
|
|
18
|
+
ListValidator,
|
|
19
|
+
NumericValidator,
|
|
20
|
+
StringValidator,
|
|
21
|
+
UrlValidator,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
from parkapi_sources.models import StaticParkingSiteInput
|
|
25
|
+
from parkapi_sources.models.enums import ParkingSiteType
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@validataclass
|
|
29
|
+
class GeojsonFeaturePropertiesInput(ValidataclassMixin):
|
|
30
|
+
uid: str = StringValidator(min_length=1, max_length=256)
|
|
31
|
+
name: str = StringValidator(min_length=1, max_length=256)
|
|
32
|
+
type: Optional[ParkingSiteType] = EnumValidator(ParkingSiteType), Default(None)
|
|
33
|
+
public_url: Optional[str] = UrlValidator(max_length=4096), Default(None)
|
|
34
|
+
address: str = StringValidator(max_length=512)
|
|
35
|
+
capacity: int = IntegerValidator()
|
|
36
|
+
has_realtime_data: bool = BooleanValidator()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@validataclass
|
|
40
|
+
class GeojsonFeatureGeometryInput:
|
|
41
|
+
type: str = AnyOfValidator(allowed_values=['Point'])
|
|
42
|
+
coordinates: list[Decimal] = ListValidator(NumericValidator(), min_length=2, max_length=2)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@validataclass
|
|
46
|
+
class GeojsonFeatureInput:
|
|
47
|
+
type: str = AnyOfValidator(allowed_values=['Feature'])
|
|
48
|
+
properties: GeojsonFeaturePropertiesInput = DataclassValidator(GeojsonFeaturePropertiesInput)
|
|
49
|
+
geometry: GeojsonFeatureGeometryInput = DataclassValidator(GeojsonFeatureGeometryInput)
|
|
50
|
+
|
|
51
|
+
def to_static_parking_site_input(self, static_data_updated_at: datetime) -> StaticParkingSiteInput:
|
|
52
|
+
return StaticParkingSiteInput(
|
|
53
|
+
lat=self.geometry.coordinates[1],
|
|
54
|
+
lon=self.geometry.coordinates[0],
|
|
55
|
+
static_data_updated_at=static_data_updated_at,
|
|
56
|
+
**self.properties.to_dict(),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@validataclass
|
|
61
|
+
class GeojsonInput:
|
|
62
|
+
type: str = AnyOfValidator(allowed_values=['FeatureCollection'])
|
|
63
|
+
features: list[dict] = ListValidator(AnythingValidator(allowed_types=[dict]))
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2024 binary butterfly GmbH
|
|
3
|
+
Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .push_converter import PushConverter # noqa: I001
|
|
7
|
+
|
|
8
|
+
from .csv_converter import CsvConverter
|
|
9
|
+
from .json_converter import JsonConverter
|
|
10
|
+
from .normalized_xlsx_converter import NormalizedXlsxConverter
|
|
11
|
+
from .xlsx_converter import XlsxConverter
|
|
12
|
+
from .xml_converter import XmlConverter
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2023 binary butterfly GmbH
|
|
3
|
+
Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import csv
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from io import StringIO
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from parkapi_sources.converters.base_converter.push import PushConverter
|
|
12
|
+
from parkapi_sources.exceptions import ImportParkingSiteException, ImportSourceException
|
|
13
|
+
from parkapi_sources.models import RealtimeParkingSiteInput, StaticParkingSiteInput
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CsvConverter(PushConverter, ABC):
|
|
17
|
+
def handle_csv_string(
|
|
18
|
+
self,
|
|
19
|
+
data: StringIO,
|
|
20
|
+
) -> tuple[list[StaticParkingSiteInput | RealtimeParkingSiteInput], list[ImportParkingSiteException]]:
|
|
21
|
+
return self.handle_csv(list(csv.reader(data, delimiter=';')))
|
|
22
|
+
|
|
23
|
+
def get_mapping_by_header(self, header_row: dict[str, str], row: list[Any]) -> dict[str, int]:
|
|
24
|
+
mapping: dict[str, int] = {}
|
|
25
|
+
for header_field, target_field in header_row.items():
|
|
26
|
+
if header_field not in row:
|
|
27
|
+
raise ImportSourceException(
|
|
28
|
+
source_uid=self.source_info.uid,
|
|
29
|
+
message=f'cannot find header key {header_field}',
|
|
30
|
+
)
|
|
31
|
+
mapping[target_field] = row.index(header_field)
|
|
32
|
+
return mapping
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def handle_csv(
|
|
36
|
+
self,
|
|
37
|
+
data: list[list],
|
|
38
|
+
) -> tuple[list[StaticParkingSiteInput | RealtimeParkingSiteInput], list[ImportParkingSiteException]]:
|
|
39
|
+
pass
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2024 binary butterfly GmbH
|
|
3
|
+
Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
|
|
8
|
+
from parkapi_sources.converters.base_converter.push import PushConverter
|
|
9
|
+
from parkapi_sources.exceptions import ImportParkingSiteException
|
|
10
|
+
from parkapi_sources.models import RealtimeParkingSiteInput, StaticParkingSiteInput
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class JsonConverter(PushConverter, ABC):
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def handle_json(
|
|
16
|
+
self,
|
|
17
|
+
data: dict | list,
|
|
18
|
+
) -> tuple[list[StaticParkingSiteInput | RealtimeParkingSiteInput], list[ImportParkingSiteException]]:
|
|
19
|
+
pass
|