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.
Files changed (86) hide show
  1. parkapi_sources/__init__.py +6 -0
  2. parkapi_sources/_version.py +16 -0
  3. parkapi_sources/converters/__init__.py +19 -0
  4. parkapi_sources/converters/bahn_v2/__init__.py +6 -0
  5. parkapi_sources/converters/bahn_v2/converter.py +72 -0
  6. parkapi_sources/converters/bahn_v2/mapper.py +38 -0
  7. parkapi_sources/converters/bahn_v2/validators.py +131 -0
  8. parkapi_sources/converters/base_converter/__init__.py +6 -0
  9. parkapi_sources/converters/base_converter/base_converter.py +26 -0
  10. parkapi_sources/converters/base_converter/pull/__init__.py +8 -0
  11. parkapi_sources/converters/base_converter/pull/pull_converter.py +20 -0
  12. parkapi_sources/converters/base_converter/pull/pull_scraper_mixin.py +74 -0
  13. parkapi_sources/converters/base_converter/pull/static_geojson_data_mixin/__init__.py +7 -0
  14. parkapi_sources/converters/base_converter/pull/static_geojson_data_mixin/mixin.py +69 -0
  15. parkapi_sources/converters/base_converter/pull/static_geojson_data_mixin/models.py +63 -0
  16. parkapi_sources/converters/base_converter/push/__init__.py +12 -0
  17. parkapi_sources/converters/base_converter/push/csv_converter.py +39 -0
  18. parkapi_sources/converters/base_converter/push/json_converter.py +19 -0
  19. parkapi_sources/converters/base_converter/push/normalized_xlsx_converter.py +105 -0
  20. parkapi_sources/converters/base_converter/push/push_converter.py +12 -0
  21. parkapi_sources/converters/base_converter/push/xlsx_converter.py +47 -0
  22. parkapi_sources/converters/base_converter/push/xml_converter.py +21 -0
  23. parkapi_sources/converters/bfrk_bw/__init__.py +7 -0
  24. parkapi_sources/converters/bfrk_bw/base_converter.py +56 -0
  25. parkapi_sources/converters/bfrk_bw/base_models.py +57 -0
  26. parkapi_sources/converters/bfrk_bw/bike_converter.py +50 -0
  27. parkapi_sources/converters/bfrk_bw/bike_models.py +56 -0
  28. parkapi_sources/converters/bfrk_bw/car_converter.py +49 -0
  29. parkapi_sources/converters/bfrk_bw/car_models.py +46 -0
  30. parkapi_sources/converters/freiburg/__init__.py +6 -0
  31. parkapi_sources/converters/freiburg/converter.py +65 -0
  32. parkapi_sources/converters/freiburg/models.py +43 -0
  33. parkapi_sources/converters/heidelberg/__init__.py +6 -0
  34. parkapi_sources/converters/heidelberg/converter.py +74 -0
  35. parkapi_sources/converters/heidelberg/validators.py +56 -0
  36. parkapi_sources/converters/karlsruhe/__init__.py +6 -0
  37. parkapi_sources/converters/karlsruhe/converter.py +89 -0
  38. parkapi_sources/converters/karlsruhe/files/ca.crt.pem +49 -0
  39. parkapi_sources/converters/karlsruhe/models.py +103 -0
  40. parkapi_sources/converters/mannheim/__init__.py +6 -0
  41. parkapi_sources/converters/mannheim/converter.py +54 -0
  42. parkapi_sources/converters/neckarsulm/__init__.py +6 -0
  43. parkapi_sources/converters/neckarsulm/converter.py +70 -0
  44. parkapi_sources/converters/neckarsulm/models.py +71 -0
  45. parkapi_sources/converters/pbw/__init__.py +6 -0
  46. parkapi_sources/converters/pbw/converter.py +117 -0
  47. parkapi_sources/converters/pbw/mapper.py +53 -0
  48. parkapi_sources/converters/pbw/validation.py +279 -0
  49. parkapi_sources/converters/pforzheim/__init__.py +6 -0
  50. parkapi_sources/converters/pforzheim/converter.py +62 -0
  51. parkapi_sources/converters/pforzheim/validation.py +43 -0
  52. parkapi_sources/converters/reutlingen/__init__.py +6 -0
  53. parkapi_sources/converters/reutlingen/converter.py +73 -0
  54. parkapi_sources/converters/reutlingen/validation.py +53 -0
  55. parkapi_sources/converters/stuttgart/__init__.py +6 -0
  56. parkapi_sources/converters/stuttgart/converter.py +132 -0
  57. parkapi_sources/converters/ulm/__init__.py +6 -0
  58. parkapi_sources/converters/ulm/converter.py +52 -0
  59. parkapi_sources/converters/vrs_p_r/__init__.py +6 -0
  60. parkapi_sources/converters/vrs_p_r/vrs_p_r.py +56 -0
  61. parkapi_sources/exceptions.py +47 -0
  62. parkapi_sources/models/__init__.py +8 -0
  63. parkapi_sources/models/enums.py +65 -0
  64. parkapi_sources/models/parking_site_inputs.py +136 -0
  65. parkapi_sources/models/source_info.py +23 -0
  66. parkapi_sources/models/xlsx_inputs.py +80 -0
  67. parkapi_sources/parkapi_sources.py +88 -0
  68. parkapi_sources/scripts/parkapi.py +210 -0
  69. parkapi_sources/util/__init__.py +8 -0
  70. parkapi_sources/util/config_helper.py +16 -0
  71. parkapi_sources/util/encoding.py +37 -0
  72. parkapi_sources/util/xml_helper.py +279 -0
  73. parkapi_sources/validators/__init__.py +13 -0
  74. parkapi_sources/validators/boolean_validators.py +24 -0
  75. parkapi_sources/validators/date_validator.py +26 -0
  76. parkapi_sources/validators/datetime_validator.py +32 -0
  77. parkapi_sources/validators/decimal_validators.py +18 -0
  78. parkapi_sources/validators/integer_validators.py +42 -0
  79. parkapi_sources/validators/noneable.py +23 -0
  80. parkapi_sources/validators/string_validators.py +32 -0
  81. parkapi_sources/validators/time_validators.py +17 -0
  82. parkapi_sources-0.2.0.dist-info/LICENCE.txt +21 -0
  83. parkapi_sources-0.2.0.dist-info/METADATA +256 -0
  84. parkapi_sources-0.2.0.dist-info/RECORD +86 -0
  85. parkapi_sources-0.2.0.dist-info/WHEEL +5 -0
  86. 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,6 @@
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 .converter import FreiburgPullConverter
@@ -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
+ )
@@ -0,0 +1,6 @@
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 .converter import HeidelbergPullConverter