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,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 .parkapi_sources import ParkAPISources
@@ -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,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 BahnV2PullConverter
@@ -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,6 @@
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 .base_converter import BaseConverter
@@ -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,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 .mixin import StaticGeojsonDataMixin
7
+ from .models import GeojsonFeatureGeometryInput, GeojsonInput
@@ -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