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,105 @@
|
|
|
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
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from openpyxl.cell import Cell
|
|
11
|
+
from openpyxl.workbook.workbook import Workbook
|
|
12
|
+
from validataclass.exceptions import ValidationError
|
|
13
|
+
|
|
14
|
+
from parkapi_sources.exceptions import ImportParkingSiteException
|
|
15
|
+
from parkapi_sources.models import StaticParkingSiteInput
|
|
16
|
+
|
|
17
|
+
from .xlsx_converter import XlsxConverter
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class NormalizedXlsxConverter(XlsxConverter, ABC):
|
|
21
|
+
"""
|
|
22
|
+
This converter acts as a base class for normalized XLSX files which always follow the same default layout. If you want to add another
|
|
23
|
+
source, you will have to subclass this converter and add `source_info` to it. If there are any minor changes, you can overwrite the
|
|
24
|
+
specific properties or methods in your child class.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
# If there are more tables with our defined format, it would make sense to move header_row to XlsxConverter
|
|
28
|
+
header_row: dict[str, str] = {
|
|
29
|
+
'ID': 'uid',
|
|
30
|
+
'Name': 'name',
|
|
31
|
+
'Art der Anlage': 'type',
|
|
32
|
+
'Betreiber Name': 'operator_name',
|
|
33
|
+
'Längengrad': 'lat',
|
|
34
|
+
'Breitengrad': 'lon',
|
|
35
|
+
'Adresse mit PLZ und Stadt': 'address',
|
|
36
|
+
'Maximale Parkdauer': 'max_stay',
|
|
37
|
+
'Anzahl Stellplätze': 'capacity',
|
|
38
|
+
'Anzahl Carsharing-Parkplätze': 'capacity_carsharing',
|
|
39
|
+
'Anzahl Ladeplätze': 'capacity_charging',
|
|
40
|
+
'Anzahl Frauenparkplätze': 'capacity_woman',
|
|
41
|
+
'Anzahl Behindertenparkplätze': 'capacity_disabled',
|
|
42
|
+
'Anlage beleuchtet?': 'has_lighting',
|
|
43
|
+
'gebührenpflichtig?': 'has_fee',
|
|
44
|
+
'Existieren Live-Daten?': 'has_realtime_data',
|
|
45
|
+
'Gebühren-Informationen': 'fee_description',
|
|
46
|
+
'Webseite': 'public_url',
|
|
47
|
+
'Park&Ride': 'is_park_and_ride',
|
|
48
|
+
'24/7 geöffnet?': 'opening_hours_is_24_7',
|
|
49
|
+
'Öffnungszeiten Mo-Fr Beginn': 'opening_hours_weekday_begin',
|
|
50
|
+
'Öffnungszeiten Mo-Fr Ende': 'opening_hours_weekday_end',
|
|
51
|
+
'Öffnungszeiten Sa Beginn': 'opening_hours_saturday_begin',
|
|
52
|
+
'Öffnungszeiten Sa Ende': 'opening_hours_saturday_end',
|
|
53
|
+
'Öffnungszeiten So Beginn': 'opening_hours_sunday_begin',
|
|
54
|
+
'Öffnungszeiten So Ende': 'opening_hours_sunday_end',
|
|
55
|
+
'Weitere öffentliche Informationen': 'description',
|
|
56
|
+
}
|
|
57
|
+
# If there are more tables with our defined format, it would make sense to move type_mapping to XlsxConverter
|
|
58
|
+
type_mapping: dict[str, str] = {
|
|
59
|
+
'Parkplatz': 'OFF_STREET_PARKING_GROUND',
|
|
60
|
+
'Parkhaus': 'CAR_PARK',
|
|
61
|
+
'Tiefgarage': 'UNDERGROUND',
|
|
62
|
+
'Am Straßenrand': 'ON_STREET',
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
def handle_xlsx(self, workbook: Workbook) -> tuple[list[StaticParkingSiteInput], list[ImportParkingSiteException]]:
|
|
66
|
+
worksheet = workbook.active
|
|
67
|
+
mapping: dict[str, int] = self.get_mapping_by_header(next(worksheet.rows))
|
|
68
|
+
|
|
69
|
+
static_parking_site_errors: list[ImportParkingSiteException] = []
|
|
70
|
+
static_parking_site_inputs: list[StaticParkingSiteInput] = []
|
|
71
|
+
|
|
72
|
+
for row in worksheet.iter_rows(min_row=2):
|
|
73
|
+
# ignore empty lines as LibreOffice sometimes adds empty rows at the end of a file
|
|
74
|
+
if row[0].value is None:
|
|
75
|
+
continue
|
|
76
|
+
parking_site_dict = self.map_row_to_parking_site_dict(mapping, row)
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
static_parking_site_inputs.append(self.static_parking_site_validator.validate(parking_site_dict))
|
|
80
|
+
except ValidationError as e:
|
|
81
|
+
static_parking_site_errors.append(
|
|
82
|
+
ImportParkingSiteException(
|
|
83
|
+
source_uid=self.source_info.uid,
|
|
84
|
+
parking_site_uid=parking_site_dict.get('uid'),
|
|
85
|
+
message=f'invalid static parking site data {parking_site_dict}: {e.to_dict()}',
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
return static_parking_site_inputs, static_parking_site_errors
|
|
91
|
+
|
|
92
|
+
def map_row_to_parking_site_dict(self, mapping: dict[str, int], row: list[Cell]) -> dict[str, Any]:
|
|
93
|
+
parking_site_raw_dict: dict[str, str] = {}
|
|
94
|
+
for field in mapping.keys():
|
|
95
|
+
parking_site_raw_dict[field] = row[mapping[field]].value
|
|
96
|
+
|
|
97
|
+
parking_site_dict = {key: value for key, value in parking_site_raw_dict.items() if not key.startswith('opening_hours_')}
|
|
98
|
+
opening_hours_input = self.excel_opening_time_validator.validate(
|
|
99
|
+
{key: value for key, value in parking_site_raw_dict.items() if key.startswith('opening_hours_')}
|
|
100
|
+
)
|
|
101
|
+
parking_site_dict['opening_hours'] = opening_hours_input.get_osm_opening_hours()
|
|
102
|
+
parking_site_dict['type'] = self.type_mapping.get(parking_site_dict.get('type'))
|
|
103
|
+
parking_site_dict['static_data_updated_at'] = datetime.now(tz=timezone.utc).isoformat()
|
|
104
|
+
|
|
105
|
+
return parking_site_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 abc import ABC
|
|
7
|
+
|
|
8
|
+
from parkapi_sources.converters import BaseConverter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PushConverter(BaseConverter, ABC):
|
|
12
|
+
pass
|
|
@@ -0,0 +1,47 @@
|
|
|
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 openpyxl.cell import Cell
|
|
9
|
+
from openpyxl.workbook import Workbook
|
|
10
|
+
from validataclass.validators import DataclassValidator
|
|
11
|
+
|
|
12
|
+
from parkapi_sources.converters.base_converter.push import PushConverter
|
|
13
|
+
from parkapi_sources.exceptions import ImportParkingSiteException, ImportSourceException
|
|
14
|
+
from parkapi_sources.models import (
|
|
15
|
+
ExcelOpeningTimeInput,
|
|
16
|
+
ExcelStaticParkingSiteInput,
|
|
17
|
+
RealtimeParkingSiteInput,
|
|
18
|
+
SourceInfo,
|
|
19
|
+
StaticParkingSiteInput,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class XlsxConverter(PushConverter, ABC):
|
|
24
|
+
static_parking_site_validator = DataclassValidator(ExcelStaticParkingSiteInput)
|
|
25
|
+
excel_opening_time_validator = DataclassValidator(ExcelOpeningTimeInput)
|
|
26
|
+
|
|
27
|
+
header_row: dict[str, str] = {}
|
|
28
|
+
source_info: SourceInfo
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def handle_xlsx(
|
|
32
|
+
self,
|
|
33
|
+
workbook: Workbook,
|
|
34
|
+
) -> tuple[list[StaticParkingSiteInput | RealtimeParkingSiteInput], list[ImportParkingSiteException]]:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
def get_mapping_by_header(self, row: tuple[Cell]) -> dict[str, int]:
|
|
38
|
+
row_values = [cell.value for cell in row]
|
|
39
|
+
mapping: dict[str, int] = {}
|
|
40
|
+
for header_col, target_field in self.header_row.items():
|
|
41
|
+
if header_col not in row_values:
|
|
42
|
+
raise ImportSourceException(
|
|
43
|
+
source_uid=self.source_info.uid,
|
|
44
|
+
message=f'cannot find header key {header_col}',
|
|
45
|
+
)
|
|
46
|
+
mapping[target_field] = row_values.index(header_col)
|
|
47
|
+
return mapping
|
|
@@ -0,0 +1,21 @@
|
|
|
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 lxml.etree import Element
|
|
9
|
+
|
|
10
|
+
from parkapi_sources.converters.base_converter.push import PushConverter
|
|
11
|
+
from parkapi_sources.exceptions import ImportParkingSiteException
|
|
12
|
+
from parkapi_sources.models import RealtimeParkingSiteInput, StaticParkingSiteInput
|
|
13
|
+
from parkapi_sources.util import XMLHelper
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class XmlConverter(PushConverter, ABC):
|
|
17
|
+
xml_helper = XMLHelper()
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def handle_xml(self, root: Element) -> tuple[list[StaticParkingSiteInput | RealtimeParkingSiteInput], list[ImportParkingSiteException]]:
|
|
21
|
+
pass
|
|
@@ -0,0 +1,7 @@
|
|
|
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 .bike_converter import BfrkBwOepnvBikePushConverter, BfrkBwSpnvBikePushConverter
|
|
7
|
+
from .car_converter import BfrkBwOepnvCarPushConverter, BfrkBwSpnvCarPushConverter
|
|
@@ -0,0 +1,56 @@
|
|
|
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 validataclass.exceptions import ValidationError
|
|
9
|
+
from validataclass.validators import DataclassValidator
|
|
10
|
+
|
|
11
|
+
from parkapi_sources.converters.base_converter.push import CsvConverter
|
|
12
|
+
from parkapi_sources.exceptions import ImportParkingSiteException
|
|
13
|
+
from parkapi_sources.models import StaticParkingSiteInput
|
|
14
|
+
|
|
15
|
+
from .base_models import BfrkBaseRowInput
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BfrkBasePushConverter(CsvConverter, ABC):
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def header_mapping(self) -> dict[str, str]:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def bfrk_row_validator(self) -> DataclassValidator:
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
def handle_csv(self, data: list[list]) -> tuple[list[StaticParkingSiteInput], list[ImportParkingSiteException]]:
|
|
31
|
+
static_parking_site_inputs: list[StaticParkingSiteInput] = []
|
|
32
|
+
static_parking_site_errors: list[ImportParkingSiteException] = []
|
|
33
|
+
|
|
34
|
+
mapping: dict[str, int] = self.get_mapping_by_header(self.header_mapping, data[0])
|
|
35
|
+
|
|
36
|
+
# We start at row 2, as the first one is our header
|
|
37
|
+
for row in data[1:]:
|
|
38
|
+
input_dict: dict[str, str] = {}
|
|
39
|
+
for field in self.header_mapping.values():
|
|
40
|
+
input_dict[field] = row[mapping[field]]
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
input_data: BfrkBaseRowInput = self.bfrk_row_validator.validate(input_dict)
|
|
44
|
+
except ValidationError as e:
|
|
45
|
+
static_parking_site_errors.append(
|
|
46
|
+
ImportParkingSiteException(
|
|
47
|
+
source_uid=self.source_info.uid,
|
|
48
|
+
parking_site_uid=input_dict.get('ID'),
|
|
49
|
+
message=f'validation error for {input_dict}: {e.to_dict()}',
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
static_parking_site_inputs.append(input_data.to_static_parking_site_input())
|
|
55
|
+
|
|
56
|
+
return static_parking_site_inputs, static_parking_site_errors
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
from decimal import Decimal
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from validataclass.dataclasses import validataclass
|
|
11
|
+
from validataclass.validators import IntegerValidator, StringValidator
|
|
12
|
+
|
|
13
|
+
from parkapi_sources.models import StaticParkingSiteInput
|
|
14
|
+
from parkapi_sources.models.enums import ExternalIdentifierType
|
|
15
|
+
from parkapi_sources.models.parking_site_inputs import ExternalIdentifierInput
|
|
16
|
+
from parkapi_sources.validators import ExcelNoneable, GermanDecimalValidator
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@validataclass
|
|
20
|
+
class BfrkBaseRowInput:
|
|
21
|
+
uid: str = StringValidator()
|
|
22
|
+
name: str = StringValidator()
|
|
23
|
+
lat: Decimal = GermanDecimalValidator()
|
|
24
|
+
lon: Decimal = GermanDecimalValidator()
|
|
25
|
+
capacity: int = IntegerValidator(allow_strings=True)
|
|
26
|
+
identifier_dhid: str = StringValidator()
|
|
27
|
+
identifier_osm: str = StringValidator()
|
|
28
|
+
photo_url: Optional[str] = ExcelNoneable(StringValidator())
|
|
29
|
+
|
|
30
|
+
def to_static_parking_site_input(self) -> StaticParkingSiteInput:
|
|
31
|
+
external_identifiers = []
|
|
32
|
+
if self.identifier_osm:
|
|
33
|
+
external_identifiers.append(
|
|
34
|
+
ExternalIdentifierInput(
|
|
35
|
+
type=ExternalIdentifierType.OSM,
|
|
36
|
+
value=self.identifier_osm,
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
if self.identifier_dhid:
|
|
40
|
+
external_identifiers.append(
|
|
41
|
+
ExternalIdentifierInput(
|
|
42
|
+
type=ExternalIdentifierType.DHID,
|
|
43
|
+
value=self.identifier_dhid,
|
|
44
|
+
),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return StaticParkingSiteInput(
|
|
48
|
+
uid=self.uid,
|
|
49
|
+
name=self.name,
|
|
50
|
+
lat=self.lat,
|
|
51
|
+
lon=self.lon,
|
|
52
|
+
address='',
|
|
53
|
+
capacity=self.capacity,
|
|
54
|
+
photo_url=self.photo_url,
|
|
55
|
+
static_data_updated_at=datetime.now(tz=timezone.utc),
|
|
56
|
+
external_identifiers=external_identifiers,
|
|
57
|
+
)
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
|
7
|
+
|
|
8
|
+
from validataclass.validators import DataclassValidator
|
|
9
|
+
|
|
10
|
+
from parkapi_sources.models import SourceInfo
|
|
11
|
+
|
|
12
|
+
from .base_converter import BfrkBasePushConverter
|
|
13
|
+
from .bike_models import BfrkBikeRowInput
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BfrkBwBikePushConverter(BfrkBasePushConverter, ABC):
|
|
17
|
+
bfrk_row_validator = DataclassValidator(BfrkBikeRowInput)
|
|
18
|
+
|
|
19
|
+
header_mapping: dict[str, str] = {
|
|
20
|
+
'ID': 'uid',
|
|
21
|
+
'HST_Name': 'name',
|
|
22
|
+
'Anlagentyp': 'type',
|
|
23
|
+
'Latitude': 'lat',
|
|
24
|
+
'Longitude': 'lon',
|
|
25
|
+
'Stellplatzanzahl': 'capacity',
|
|
26
|
+
'beleuchtet': 'has_lighting',
|
|
27
|
+
'kostenpflichtig': 'has_fee',
|
|
28
|
+
'ueberdacht': 'has_roof',
|
|
29
|
+
'HST_DHID': 'identifier_dhid',
|
|
30
|
+
'OSM_ID': 'identifier_osm',
|
|
31
|
+
'Anlage_Foto': 'photo_url',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BfrkBwOepnvBikePushConverter(BfrkBwBikePushConverter):
|
|
36
|
+
source_info = SourceInfo(
|
|
37
|
+
uid='bfrk_bw_oepnv_bike',
|
|
38
|
+
name='Barrierefreie Reisekette Baden-Württemberg: Fahrrad-Parkplätze an Bushaltestellen',
|
|
39
|
+
public_url='https://www.mobidata-bw.de/dataset/bfrk-barrierefreiheit-an-bw-haltestellen',
|
|
40
|
+
has_realtime_data=False,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class BfrkBwSpnvBikePushConverter(BfrkBwBikePushConverter):
|
|
45
|
+
source_info = SourceInfo(
|
|
46
|
+
uid='bfrk_bw_spnv_bike',
|
|
47
|
+
name='Barrierefreie Reisekette Baden-Württemberg: Fahrrad-Parkplätze an Bahnhöfen',
|
|
48
|
+
public_url='https://www.mobidata-bw.de/dataset/bfrk-barrierefreiheit-an-bw-bahnhoefen',
|
|
49
|
+
has_realtime_data=False,
|
|
50
|
+
)
|
|
@@ -0,0 +1,56 @@
|
|
|
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 enum import Enum
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from validataclass.dataclasses import validataclass
|
|
10
|
+
from validataclass.validators import EnumValidator
|
|
11
|
+
|
|
12
|
+
from parkapi_sources.models.enums import ParkingSiteType, PurposeType
|
|
13
|
+
from parkapi_sources.models.parking_site_inputs import StaticParkingSiteInput
|
|
14
|
+
from parkapi_sources.validators import ExcelNoneable, MappedBooleanValidator
|
|
15
|
+
|
|
16
|
+
from .base_models import BfrkBaseRowInput
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GermanMappedBooleanValidator(MappedBooleanValidator):
|
|
20
|
+
def __init__(self, *args, **kwargs):
|
|
21
|
+
super().__init__(*args, mapping={'ja': True, 'nein': False}, **kwargs)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BfrkBikeType(Enum):
|
|
25
|
+
WALL_LOOPS = 'Vorderradhalter'
|
|
26
|
+
STANDS = 'Anlehnbuegel'
|
|
27
|
+
LOCKERS = 'Fahrradboxen'
|
|
28
|
+
TWO_TIER = 'doppelstoeckig'
|
|
29
|
+
SHED = 'Fahrradsammelanlage'
|
|
30
|
+
BUILDING = 'Fahrradparkhaus'
|
|
31
|
+
AUTOMATIC_BUILDING = 'automatischesParksystem'
|
|
32
|
+
OTHER = 'Sonstiges'
|
|
33
|
+
|
|
34
|
+
def to_parking_site_type(self) -> ParkingSiteType:
|
|
35
|
+
if self == BfrkBikeType.AUTOMATIC_BUILDING:
|
|
36
|
+
return ParkingSiteType.BUILDING
|
|
37
|
+
return ParkingSiteType[self.name]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@validataclass
|
|
41
|
+
class BfrkBikeRowInput(BfrkBaseRowInput):
|
|
42
|
+
type: BfrkBikeType = EnumValidator(BfrkBikeType)
|
|
43
|
+
has_roof: Optional[bool] = ExcelNoneable(GermanMappedBooleanValidator())
|
|
44
|
+
has_fee: bool = GermanMappedBooleanValidator()
|
|
45
|
+
has_lighting: bool = GermanMappedBooleanValidator()
|
|
46
|
+
|
|
47
|
+
def to_static_parking_site_input(self) -> StaticParkingSiteInput:
|
|
48
|
+
static_parking_site_input = super().to_static_parking_site_input()
|
|
49
|
+
|
|
50
|
+
static_parking_site_input.type = self.type.to_parking_site_type()
|
|
51
|
+
static_parking_site_input.has_roof = self.has_roof
|
|
52
|
+
static_parking_site_input.has_fee = self.has_fee
|
|
53
|
+
static_parking_site_input.has_lighting = self.has_lighting
|
|
54
|
+
static_parking_site_input.purpose = PurposeType.BIKE
|
|
55
|
+
|
|
56
|
+
return static_parking_site_input
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
|
7
|
+
|
|
8
|
+
from validataclass.validators import DataclassValidator
|
|
9
|
+
|
|
10
|
+
from parkapi_sources.models import SourceInfo
|
|
11
|
+
|
|
12
|
+
from .base_converter import BfrkBasePushConverter
|
|
13
|
+
from .car_models import BfrkCarRowInput
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BfrkBwCarPushConverter(BfrkBasePushConverter, ABC):
|
|
17
|
+
bfrk_row_validator = DataclassValidator(BfrkCarRowInput)
|
|
18
|
+
|
|
19
|
+
header_mapping: dict[str, str] = {
|
|
20
|
+
'ID': 'uid',
|
|
21
|
+
'HST_Name': 'name',
|
|
22
|
+
'Art': 'type',
|
|
23
|
+
'Latitude': 'lat',
|
|
24
|
+
'Longitude': 'lon',
|
|
25
|
+
'Stellplatzanzahl_insgesamt': 'capacity',
|
|
26
|
+
'Stellplatzanzahl_Behinderte': 'capacity_disabled',
|
|
27
|
+
'Nutzungsbedingungen': 'description',
|
|
28
|
+
'HST_DHID': 'identifier_dhid',
|
|
29
|
+
'OSM_ID': 'identifier_osm',
|
|
30
|
+
'Parkplatz_Foto': 'photo_url',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class BfrkBwOepnvCarPushConverter(BfrkBwCarPushConverter):
|
|
35
|
+
source_info = SourceInfo(
|
|
36
|
+
uid='bfrk_bw_oepnv_car',
|
|
37
|
+
name='Barrierefreie Reisekette Baden-Württemberg: PKW-Parkplätze an Bushaltestellen',
|
|
38
|
+
public_url='https://www.mobidata-bw.de/dataset/bfrk-barrierefreiheit-an-bw-haltestellen',
|
|
39
|
+
has_realtime_data=False,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class BfrkBwSpnvCarPushConverter(BfrkBwCarPushConverter):
|
|
44
|
+
source_info = SourceInfo(
|
|
45
|
+
uid='bfrk_bw_spnv_car',
|
|
46
|
+
name='Barrierefreie Reisekette Baden-Württemberg: PKW-Parkplätze an Bahnhöfen',
|
|
47
|
+
public_url='https://www.mobidata-bw.de/dataset/bfrk-barrierefreiheit-an-bw-bahnhoefen',
|
|
48
|
+
has_realtime_data=False,
|
|
49
|
+
)
|
|
@@ -0,0 +1,46 @@
|
|
|
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 enum import Enum
|
|
7
|
+
|
|
8
|
+
from validataclass.dataclasses import validataclass
|
|
9
|
+
from validataclass.validators import EnumValidator, IntegerValidator
|
|
10
|
+
|
|
11
|
+
from parkapi_sources.models import StaticParkingSiteInput
|
|
12
|
+
from parkapi_sources.models.enums import ParkingSiteType
|
|
13
|
+
from parkapi_sources.validators import ReplacingStringValidator
|
|
14
|
+
|
|
15
|
+
from .base_models import BfrkBaseRowInput
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BfrkCarType(Enum):
|
|
19
|
+
PARK_AND_RIDE_PARKING_SITE = 'Park-and-Ride Parkplatz'
|
|
20
|
+
SHORT_TERM_PARKING_SITE = 'Kurzzeitparkplätze'
|
|
21
|
+
CAR_PARK = 'Parkhaus'
|
|
22
|
+
DISABLED_PARKING_SPACE = 'Behindertenparkplatz'
|
|
23
|
+
|
|
24
|
+
def to_parking_site_type(self) -> ParkingSiteType:
|
|
25
|
+
return {
|
|
26
|
+
self.PARK_AND_RIDE_PARKING_SITE: ParkingSiteType.OFF_STREET_PARKING_GROUND,
|
|
27
|
+
self.SHORT_TERM_PARKING_SITE: ParkingSiteType.OFF_STREET_PARKING_GROUND,
|
|
28
|
+
self.CAR_PARK: ParkingSiteType.CAR_PARK,
|
|
29
|
+
self.DISABLED_PARKING_SPACE: ParkingSiteType.OTHER,
|
|
30
|
+
}.get(self)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@validataclass
|
|
34
|
+
class BfrkCarRowInput(BfrkBaseRowInput):
|
|
35
|
+
type: BfrkCarType = EnumValidator(BfrkCarType)
|
|
36
|
+
capacity_disabled: int = IntegerValidator(allow_strings=True)
|
|
37
|
+
description: str = ReplacingStringValidator(mapping={'\x80': '€'})
|
|
38
|
+
|
|
39
|
+
def to_static_parking_site_input(self) -> StaticParkingSiteInput:
|
|
40
|
+
static_parking_site_input = super().to_static_parking_site_input()
|
|
41
|
+
|
|
42
|
+
static_parking_site_input.type = self.type.to_parking_site_type()
|
|
43
|
+
static_parking_site_input.capacity_disabled = self.capacity_disabled
|
|
44
|
+
static_parking_site_input.description = self.description
|
|
45
|
+
|
|
46
|
+
return static_parking_site_input
|
|
@@ -0,0 +1,65 @@
|
|
|
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, StaticGeojsonDataMixin
|
|
11
|
+
from parkapi_sources.exceptions import ImportParkingSiteException, ImportSourceException
|
|
12
|
+
from parkapi_sources.models import RealtimeParkingSiteInput, SourceInfo, StaticParkingSiteInput
|
|
13
|
+
|
|
14
|
+
from .models import FreiburgFeatureInput
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FreiburgPullConverter(PullConverter, StaticGeojsonDataMixin):
|
|
18
|
+
freiburg_realtime_feature_validator = DataclassValidator(FreiburgFeatureInput)
|
|
19
|
+
source_info = SourceInfo(
|
|
20
|
+
uid='freiburg',
|
|
21
|
+
name='Stadt Freiburg',
|
|
22
|
+
public_url='https://www.freiburg.de/pb/,Lde/231355.html',
|
|
23
|
+
source_url='https://geoportal.freiburg.de/wfs/gdm_pls/gdm_plslive?request=getfeature&service=wfs&version=1.1.0&typename=pls'
|
|
24
|
+
'&outputformat=geojson&srsname=epsg:4326',
|
|
25
|
+
timezone='Europe/Berlin',
|
|
26
|
+
attribution_contributor='Stadt Freiburg',
|
|
27
|
+
attribution_license='dl-de/by-2-0',
|
|
28
|
+
has_realtime_data=True,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def get_static_parking_sites(self) -> tuple[list[StaticParkingSiteInput], list[ImportParkingSiteException]]:
|
|
32
|
+
return self._get_static_parking_site_inputs_and_exceptions(source_uid=self.source_info.uid)
|
|
33
|
+
|
|
34
|
+
def get_realtime_parking_sites(self) -> tuple[list[RealtimeParkingSiteInput], list[ImportParkingSiteException]]:
|
|
35
|
+
realtime_parking_site_inputs: list[RealtimeParkingSiteInput] = []
|
|
36
|
+
import_parking_site_exceptions: list[ImportParkingSiteException] = []
|
|
37
|
+
|
|
38
|
+
response = requests.get(self.source_info.source_url, timeout=30)
|
|
39
|
+
response_data = response.json()
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
realtime_input = self.geojson_validator.validate(response_data)
|
|
43
|
+
except ValidationError as e:
|
|
44
|
+
raise ImportSourceException(
|
|
45
|
+
source_uid=self.source_info.uid,
|
|
46
|
+
message=f'Invalid Input at source {self.source_info.uid}: {e.to_dict()}, data: {response_data}',
|
|
47
|
+
) from e
|
|
48
|
+
|
|
49
|
+
for update_dict in realtime_input.features:
|
|
50
|
+
try:
|
|
51
|
+
update_input = self.freiburg_realtime_feature_validator.validate(update_dict)
|
|
52
|
+
except ValidationError as e:
|
|
53
|
+
import_parking_site_exceptions.append(
|
|
54
|
+
ImportParkingSiteException(
|
|
55
|
+
source_uid=self.source_info.uid,
|
|
56
|
+
parking_site_uid=update_dict.get('properties').get('obs_parkid'),
|
|
57
|
+
message=f'Invalid data at uid {update_dict.get("properties").get("obs_parkid")}: '
|
|
58
|
+
f'{e.to_dict()}, data: {update_dict}',
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
realtime_parking_site_inputs.append(update_input.to_realtime_parking_site_input())
|
|
64
|
+
|
|
65
|
+
return realtime_parking_site_inputs, import_parking_site_exceptions
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
from zoneinfo import ZoneInfo
|
|
8
|
+
|
|
9
|
+
from validataclass.dataclasses import validataclass
|
|
10
|
+
from validataclass.validators import DataclassValidator, IntegerValidator, StringValidator
|
|
11
|
+
|
|
12
|
+
from parkapi_sources.models import RealtimeParkingSiteInput
|
|
13
|
+
from parkapi_sources.models.enums import OpeningStatus
|
|
14
|
+
from parkapi_sources.validators import SpacedDateTimeValidator
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@validataclass
|
|
18
|
+
class FreiburgPropertiesInput:
|
|
19
|
+
obs_id: int = IntegerValidator(allow_strings=True)
|
|
20
|
+
obs_parkid: int = IntegerValidator(allow_strings=True)
|
|
21
|
+
obs_state: int = IntegerValidator(allow_strings=True)
|
|
22
|
+
obs_max: int = IntegerValidator(allow_strings=True)
|
|
23
|
+
obs_free: int = IntegerValidator(allow_strings=True)
|
|
24
|
+
obs_ts: datetime = SpacedDateTimeValidator(
|
|
25
|
+
local_timezone=ZoneInfo('America/Los_Angeles'),
|
|
26
|
+
target_timezone=timezone.utc,
|
|
27
|
+
)
|
|
28
|
+
park_name: str = StringValidator()
|
|
29
|
+
park_id: str = StringValidator()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@validataclass
|
|
33
|
+
class FreiburgFeatureInput:
|
|
34
|
+
properties: FreiburgPropertiesInput = DataclassValidator(FreiburgPropertiesInput)
|
|
35
|
+
|
|
36
|
+
def to_realtime_parking_site_input(self) -> RealtimeParkingSiteInput:
|
|
37
|
+
return RealtimeParkingSiteInput(
|
|
38
|
+
uid=str(self.properties.obs_parkid),
|
|
39
|
+
realtime_capacity=self.properties.obs_max,
|
|
40
|
+
realtime_free_capacity=self.properties.obs_free,
|
|
41
|
+
realtime_data_updated_at=self.properties.obs_ts,
|
|
42
|
+
realtime_opening_status=OpeningStatus.OPEN if self.properties.obs_state else OpeningStatus.CLOSED,
|
|
43
|
+
)
|