From f7d765fa1cbe5884319bff63c07257b4768fc8d5 Mon Sep 17 00:00:00 2001 From: Abdullahi Fatola Date: Wed, 18 Sep 2024 15:30:58 +0200 Subject: [PATCH 1/3] Integrate Pull converter VRN P+R --- README.md | 3 + src/parkapi_sources/converters/__init__.py | 1 + .../converters/vrn_p_r/__init__.py | 7 + .../converters/vrn_p_r/converter.py | 142 ++++ .../converters/vrn_p_r/models.py | 57 ++ .../converters/vrn_p_r/vrn_converter.py | 85 +++ .../converters/vrn_p_r/vrn_models.py | 111 +++ src/parkapi_sources/parkapi_sources.py | 6 + tests/converters/data/vrn_p_r.json | 708 ++++++++++++++++++ tests/converters/data/vrn_p_r_multiguide.json | 611 +++++++++++++++ tests/converters/data/vrn_p_r_sonah.json | 305 ++++++++ tests/converters/vrn_p_r_test.py | 114 +++ 12 files changed, 2150 insertions(+) create mode 100644 src/parkapi_sources/converters/vrn_p_r/__init__.py create mode 100644 src/parkapi_sources/converters/vrn_p_r/converter.py create mode 100644 src/parkapi_sources/converters/vrn_p_r/models.py create mode 100644 src/parkapi_sources/converters/vrn_p_r/vrn_converter.py create mode 100644 src/parkapi_sources/converters/vrn_p_r/vrn_models.py create mode 100644 tests/converters/data/vrn_p_r.json create mode 100644 tests/converters/data/vrn_p_r_multiguide.json create mode 100644 tests/converters/data/vrn_p_r_sonah.json create mode 100644 tests/converters/vrn_p_r_test.py diff --git a/README.md b/README.md index 725c2f3..a340944 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,9 @@ We support following data sources: | Stadt Reutlingen: Fahrrad-Abstellanlagen | bike | push (csv) | `reutlingen_bike` | no | | Stadt Stuttgart | car | push (json) | `stuttgart` | yes | | Stadt Ulm | car | pull | `ulm` | yes | +| Verkehrsverbund Rhein-Neckar GmbH: P+R Parkplätze | car | pull | `vrn_p_r` | yes | +| Verkehrsverbund Rhein-Neckar GmbH: Multiguide API - P+R Parkplätze | car | pull | `vrn_p_r_multiguide` | yes | +| Verkehrsverbund Rhein-Neckar GmbH: Sonah API - P+R Parkplätze | car | pull | `vrn_p_r_sonah` | yes | | Verband Region Stuttgart: Bondorf | car | pull | `vrs_bondorf` | yes | | Verband Region Stuttgart: Kirchheim | car | pull | `vrs_kirchheim` | yes | | Verband Region Stuttgart: Neustadt | car | pull | `vrs_neustadt` | yes | diff --git a/src/parkapi_sources/converters/__init__.py b/src/parkapi_sources/converters/__init__.py index 42d70aa..d6b30e5 100644 --- a/src/parkapi_sources/converters/__init__.py +++ b/src/parkapi_sources/converters/__init__.py @@ -39,5 +39,6 @@ from .reutlingen_bike import ReutlingenBikePushConverter from .stuttgart import StuttgartPushConverter from .ulm import UlmPullConverter +from .vrn_p_r import VrnParkAndRideMultiguidePullConverter, VrnParkAndRidePullConverter, VrnParkAndRideSonahPullConverter from .vrs import VrsBondorfPullConverter, VrsKirchheimPullConverter, VrsNeustadtPullConverter, VrsVaihingenPullConverter from .vrs_p_r import VrsParkAndRidePushConverter diff --git a/src/parkapi_sources/converters/vrn_p_r/__init__.py b/src/parkapi_sources/converters/vrn_p_r/__init__.py new file mode 100644 index 0000000..da0d944 --- /dev/null +++ b/src/parkapi_sources/converters/vrn_p_r/__init__.py @@ -0,0 +1,7 @@ +""" +Copyright 2024 binary butterfly GmbH +Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt. +""" + +from .converter import VrnParkAndRideMultiguidePullConverter, VrnParkAndRideSonahPullConverter +from .vrn_converter import VrnParkAndRidePullConverter diff --git a/src/parkapi_sources/converters/vrn_p_r/converter.py b/src/parkapi_sources/converters/vrn_p_r/converter.py new file mode 100644 index 0000000..f8b4428 --- /dev/null +++ b/src/parkapi_sources/converters/vrn_p_r/converter.py @@ -0,0 +1,142 @@ +""" +Copyright 2024 binary butterfly GmbH +Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt. +""" + +import requests +from validataclass.exceptions import ValidationError +from validataclass.validators import AnythingValidator, DataclassValidator, ListValidator + +from parkapi_sources.exceptions import ImportParkingSiteException, ImportSourceException +from parkapi_sources.models import RealtimeParkingSiteInput, SourceInfo, StaticParkingSiteInput + +from .models import VrnParkAndRideMultiguideInput, VrnParkAndRideSonahInput +from .vrn_converter import VrnParkAndRidePullConverter + + +class VrnParkAndRideMultiguidePullConverter(VrnParkAndRidePullConverter): + list_validator = ListValidator(AnythingValidator(allowed_types=[dict])) + vrn_multiguide_validator = DataclassValidator(VrnParkAndRideMultiguideInput) + + source_info = SourceInfo( + uid='vrn_p_r_multiguide', + name='Verkehrsverbund Rhein-Neckar GmbH: Multiguide API - P+R Parkplätze', + source_url='https://vrn.multiguide.info/api/area', + has_realtime_data=True, # ATM it's impossible to get realtime data due rate limit restrictions + ) + + def get_static_parking_sites(self) -> tuple[list[StaticParkingSiteInput], list[ImportParkingSiteException]]: + return self._get_raw_static_parking_sites() + + def get_realtime_parking_sites(self) -> tuple[list[RealtimeParkingSiteInput], list[ImportParkingSiteException]]: + realtime_parking_site_inputs: list[RealtimeParkingSiteInput] = [] + + realtime_multiguide_inputs, import_parking_site_exceptions = self._get_raw_realtime_parking_sites() + + for realtime_multiguide_input in realtime_multiguide_inputs: + realtime_parking_site_inputs.append(realtime_multiguide_input.to_realtime_parking_site_input()) + + return realtime_parking_site_inputs, import_parking_site_exceptions + + def _get_raw_realtime_parking_sites(self) -> tuple[list[VrnParkAndRideMultiguideInput], list[ImportParkingSiteException]]: + vrn_multiguide_inputs: list[VrnParkAndRideMultiguideInput] = [] + import_parking_site_exceptions: list[ImportParkingSiteException] = [] + + response = requests.get( + url=self.source_info.source_url, + auth=( + self.config_helper.get('PARK_API_VRN_P_R_MULTIGUIDE_USERNAME'), + self.config_helper.get('PARK_API_VRN_P_R_MULTIGUIDE_PASSWORD'), + ), + timeout=30, + ) + response_data = response.json() + try: + input_dicts = self.list_validator.validate(response_data) + except ValidationError as e: + raise ImportSourceException( + source_uid=self.source_info.uid, + message=f'Invalid Input at source {self.source_info.uid}: {e.to_dict()}, data: {response_data}', + ) from e + + for input_dict in input_dicts: + try: + vrn_multiguide_input = self.vrn_multiguide_validator.validate(input_dict) + except ValidationError as e: + import_parking_site_exceptions.append( + ImportParkingSiteException( + source_uid=self.source_info.uid, + parking_site_uid=input_dict.get('Id'), + message=f'Invalid data at uid {input_dict.get("Id")}: {e.to_dict()}, ' f'data: {input_dict}', + ), + ) + continue + + vrn_multiguide_inputs.append(vrn_multiguide_input) + + return vrn_multiguide_inputs, import_parking_site_exceptions + + +class VrnParkAndRideSonahPullConverter(VrnParkAndRidePullConverter): + list_validator = ListValidator(AnythingValidator(allowed_types=[dict])) + vrn_sonah_validator = DataclassValidator(VrnParkAndRideSonahInput) + + source_info = SourceInfo( + uid='vrn_p_r_sonah', + name='Verkehrsverbund Rhein-Neckar GmbH: Sonah API - P+R Parkplätze', + source_url='https://vrnm.dyndns.sonah.xyz/api/v3/rest/json/locations', + has_realtime_data=True, # ATM it's impossible to get realtime data due rate limit restrictions + ) + + def get_static_parking_sites(self) -> tuple[list[StaticParkingSiteInput], list[ImportParkingSiteException]]: + return self._get_raw_static_parking_sites() + + def get_realtime_parking_sites(self) -> tuple[list[RealtimeParkingSiteInput], list[ImportParkingSiteException]]: + realtime_parking_site_inputs: list[RealtimeParkingSiteInput] = [] + + realtime_sonah_inputs, import_parking_site_exceptions = self._get_raw_realtime_parking_sites() + + for realtime_sonah_input in realtime_sonah_inputs: + realtime_parking_site_inputs.append(realtime_sonah_input.to_realtime_parking_site_input()) + + return realtime_parking_site_inputs, import_parking_site_exceptions + + def _get_raw_realtime_parking_sites(self) -> tuple[list[VrnParkAndRideSonahInput], list[ImportParkingSiteException]]: + vrn_sonah_inputs: list[VrnParkAndRideSonahInput] = [] + import_parking_site_exceptions: list[ImportParkingSiteException] = [] + + headers: dict[str, str] = { + 'Accept': 'application/json', + 'Authorization': self.config_helper.get('PARK_API_VRN_P_R_SONAH_BEARER_TOKEN'), + } + + response = requests.get( + self.source_info.source_url, + headers=headers, + timeout=30, + ) + response_data = response.json() + try: + input_dicts = self.list_validator.validate(response_data) + except ValidationError as e: + raise ImportSourceException( + source_uid=self.source_info.uid, + message=f'Invalid Input at source {self.source_info.uid}: {e.to_dict()}, data: {response_data}', + ) from e + + for input_dict in input_dicts: + try: + vrn_sonah_input = self.vrn_sonah_validator.validate(input_dict) + except ValidationError as e: + import_parking_site_exceptions.append( + ImportParkingSiteException( + source_uid=self.source_info.uid, + parking_site_uid=input_dict.get('LocationID'), + message=f'Invalid data at uid {input_dict.get("LocationID")}: {e.to_dict()}, ' f'data: {input_dict}', + ), + ) + continue + + vrn_sonah_inputs.append(vrn_sonah_input) + + return vrn_sonah_inputs, import_parking_site_exceptions diff --git a/src/parkapi_sources/converters/vrn_p_r/models.py b/src/parkapi_sources/converters/vrn_p_r/models.py new file mode 100644 index 0000000..26a7aa2 --- /dev/null +++ b/src/parkapi_sources/converters/vrn_p_r/models.py @@ -0,0 +1,57 @@ +""" +Copyright 2024 binary butterfly GmbH +Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt. +""" + +from datetime import datetime, timezone + +from validataclass.dataclasses import validataclass +from validataclass.exceptions import ValidationError +from validataclass.validators import IntegerValidator, NumericValidator, StringValidator + +from parkapi_sources.models import RealtimeParkingSiteInput + + +@validataclass +class VrnParkAndRideMultiguideInput: + Name: str = StringValidator() + Id: int = IntegerValidator(allow_strings=True) + Constructed: int = IntegerValidator(allow_strings=True) + Available: int = IntegerValidator(allow_strings=True) + Free: int = IntegerValidator(allow_strings=True) + Occupied: int = IntegerValidator(allow_strings=True) + Reserved: int = IntegerValidator(allow_strings=True) + Defect: int = IntegerValidator(allow_strings=True) + + def __post_init__(self): + if self.Constructed < self.Occupied: + raise ValidationError(reason='More occupied sites than capacity') + + def to_realtime_parking_site_input(self) -> RealtimeParkingSiteInput: + return RealtimeParkingSiteInput( + uid=self.Name, + realtime_capacity=self.Constructed, + realtime_free_capacity=self.Free, + realtime_data_updated_at=datetime.now(timezone.utc), + ) + + +@validataclass +class VrnParkAndRideSonahInput: + Name: str = StringValidator() + LocationID: int = IntegerValidator() + TotalParking: int = NumericValidator() + FreeParking: int = NumericValidator() + OccupiedParking: int = NumericValidator() + + def __post_init__(self): + if self.TotalParking < self.OccupiedParking: + raise ValidationError(reason='More occupied sites than capacity') + + def to_realtime_parking_site_input(self) -> RealtimeParkingSiteInput: + return RealtimeParkingSiteInput( + uid=self.Name, + realtime_capacity=int(self.TotalParking), + realtime_free_capacity=int(self.FreeParking), + realtime_data_updated_at=datetime.now(timezone.utc), + ) diff --git a/src/parkapi_sources/converters/vrn_p_r/vrn_converter.py b/src/parkapi_sources/converters/vrn_p_r/vrn_converter.py new file mode 100644 index 0000000..9e2c5e1 --- /dev/null +++ b/src/parkapi_sources/converters/vrn_p_r/vrn_converter.py @@ -0,0 +1,85 @@ +""" +Copyright 2024 binary butterfly GmbH +Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt. +""" + +import requests +from validataclass.exceptions import ValidationError +from validataclass.validators import DataclassValidator + +from parkapi_sources.converters.base_converter.pull import GeojsonInput, PullConverter +from parkapi_sources.exceptions import ImportParkingSiteException, ImportSourceException +from parkapi_sources.models import RealtimeParkingSiteInput, SourceInfo, StaticParkingSiteInput + +from .vrn_models import VrnParkAndRideFeaturesInput + + +class VrnParkAndRidePullConverter(PullConverter): + geojson_validator = DataclassValidator(GeojsonInput) + vrn_p_r_feature_validator = DataclassValidator(VrnParkAndRideFeaturesInput) + + source_info = SourceInfo( + uid='vrn_p_r', + name='Verkehrsverbund Rhein-Neckar GmbH - P+R Parkplätze', + public_url='https://www.vrn.de/opendata/datasets/pr-parkplaetze-mit-vrn-parksensorik', + source_url='https://spatial.vrn.de/data/rest/services/Hosted/p_r_parkapi_static/FeatureServer/5/query?where=objectid%3E0&outFields=*&returnGeometry=true&f=geojson', + timezone='Europe/Berlin', + has_realtime_data=False, + ) + + def _get_feature_inputs(self) -> tuple[list[VrnParkAndRideFeaturesInput], list[ImportParkingSiteException]]: + feature_inputs: list[VrnParkAndRideFeaturesInput] = [] + import_parking_site_exceptions: list[ImportParkingSiteException] = [] + + response = requests.get(self.source_info.source_url, timeout=30) + response_data = response.json() + + try: + geojson_input = self.geojson_validator.validate(response_data) + except ValidationError as e: + raise ImportSourceException( + source_uid=self.source_info.uid, + message=f'Invalid Input at source {self.source_info.uid}: {e.to_dict()}, data: {response_data}', + ) from e + + for feature_dict in geojson_input.features: + if self._should_ignore_dataset(feature_dict): + continue + + try: + feature_input = self.vrn_p_r_feature_validator.validate(feature_dict) + except ValidationError as e: + import_parking_site_exceptions.append( + ImportParkingSiteException( + source_uid=self.source_info.uid, + parking_site_uid=feature_dict.get('properties', {}).get('id'), + message=f'Invalid data at uid {feature_dict.get("properties", {}).get("id")}: ' + f'{e.to_dict()}, data: {feature_dict}', + ), + ) + continue + + feature_inputs.append(feature_input) + + return feature_inputs, import_parking_site_exceptions + + def _should_ignore_dataset(self, feature_dict: dict) -> bool: + if self.config_helper.get('PARK_API_Vrn_P_R_IGNORE_MISSING_CAPACITIES'): + return feature_dict.get('properties', {}).get('capacity') is None + + return False + + def get_static_parking_sites(self) -> tuple[list[StaticParkingSiteInput], list[ImportParkingSiteException]]: + return self._get_raw_static_parking_sites() + + def _get_raw_static_parking_sites(self) -> tuple[list[StaticParkingSiteInput], list[ImportParkingSiteException]]: + feature_inputs, import_parking_site_exceptions = self._get_feature_inputs() + + static_parking_site_inputs: list[StaticParkingSiteInput] = [] + for feature_input in feature_inputs: + static_parking_site_inputs.append(feature_input.to_static_parking_site_input()) + + return static_parking_site_inputs, import_parking_site_exceptions + + def get_realtime_parking_sites(self) -> tuple[list[RealtimeParkingSiteInput], list[ImportParkingSiteException]]: + return [], [] diff --git a/src/parkapi_sources/converters/vrn_p_r/vrn_models.py b/src/parkapi_sources/converters/vrn_p_r/vrn_models.py new file mode 100644 index 0000000..d010b80 --- /dev/null +++ b/src/parkapi_sources/converters/vrn_p_r/vrn_models.py @@ -0,0 +1,111 @@ +""" +Copyright 2024 binary butterfly GmbH +Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt. +""" + +from datetime import date, datetime, timezone +from decimal import Decimal +from enum import Enum + +from validataclass.dataclasses import DefaultUnset, ValidataclassMixin, validataclass +from validataclass.helpers import OptionalUnset, UnsetValue +from validataclass.validators import ( + DataclassValidator, + EnumValidator, + IntegerValidator, + NoneToUnsetValue, + NumericValidator, + StringValidator, + UrlValidator, +) + +from parkapi_sources.converters.base_converter.pull import GeojsonFeatureGeometryInput +from parkapi_sources.models import StaticParkingSiteInput +from parkapi_sources.models.enums import ParkingSiteType, PurposeType +from parkapi_sources.validators import MappedBooleanValidator, ParsedDateValidator, TimestampDateTimeValidator + + +class VrnParkAndRideType(Enum): + CAR_PARK = 'Parkhaus' + OFF_STREET_PARKING_GROUND = 'Parkplatz' + + def to_parking_site_type(self) -> ParkingSiteType: + return { + self.CAR_PARK: ParkingSiteType.CAR_PARK, + self.OFF_STREET_PARKING_GROUND: ParkingSiteType.OFF_STREET_PARKING_GROUND, + }.get(self) + + +@validataclass +class VrnParkAndRidePropertiesInput(ValidataclassMixin): + original_uid: str = StringValidator(min_length=1, max_length=256) + name: str = StringValidator(min_length=0, max_length=256) + type: OptionalUnset[VrnParkAndRideType] = NoneToUnsetValue(EnumValidator(VrnParkAndRideType)), DefaultUnset + public_url: OptionalUnset[str] = NoneToUnsetValue(UrlValidator(max_length=4096)), DefaultUnset + photo_url: OptionalUnset[str] = NoneToUnsetValue(UrlValidator(max_length=4096)), DefaultUnset + lat: OptionalUnset[Decimal] = NumericValidator() + lon: OptionalUnset[Decimal] = NumericValidator() + address: OptionalUnset[str] = NoneToUnsetValue(StringValidator(min_length=0, max_length=256)), DefaultUnset + description: OptionalUnset[str] = NoneToUnsetValue(StringValidator(max_length=512)), DefaultUnset + operator_name: OptionalUnset[str] = NoneToUnsetValue(StringValidator(min_length=0, max_length=256)), DefaultUnset + + capacity: int = IntegerValidator(min_value=0) + capacity_charging: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset + capacity_family: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset + capacity_woman: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset + capacity_bus: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset + capacity_truck: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset + capacity_carsharing: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset + capacity_disabled: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset + max_height: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset + # supervision_type: OptionalUnset[HerrenbergBikeSupervisionType] = ( + # NoneToUnsetValue(EnumValidator(HerrenbergBikeSupervisionType)), + # DefaultUnset, + # ) + has_realtime_data: OptionalUnset[bool] = NoneToUnsetValue(MappedBooleanValidator(mapping={'ja': True, 'nein': False})), DefaultUnset + vrn_sensor_id: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset + realtime_opening_status: OptionalUnset[str] = NoneToUnsetValue(StringValidator(min_length=0, max_length=256)), DefaultUnset + created_at: OptionalUnset[date] = NoneToUnsetValue(ParsedDateValidator(date_format='%Y-%m-%d')), DefaultUnset + # modified_at: OptionalUnset[date] = NoneToUnsetValue(ParsedDateValidator(date_format='%Y-%m-%d')), DefaultUnset + static_data_updated_at: OptionalUnset[datetime] = ( + NoneToUnsetValue(TimestampDateTimeValidator(allow_strings=True, divisor=1000)), + DefaultUnset, + ) + has_lighting: OptionalUnset[bool] = NoneToUnsetValue(MappedBooleanValidator(mapping={'ja': True, 'nein': False})), DefaultUnset + has_fee: OptionalUnset[bool] = NoneToUnsetValue(MappedBooleanValidator(mapping={'ja': True, 'nein': False})), DefaultUnset + is_covered: OptionalUnset[bool] = NoneToUnsetValue(MappedBooleanValidator(mapping={'ja': True, 'nein': False})), DefaultUnset + related_location: OptionalUnset[str] = NoneToUnsetValue(StringValidator(min_length=0, max_length=256)), DefaultUnset + opening_hours: OptionalUnset[str] = NoneToUnsetValue(StringValidator(min_length=0, max_length=256)), DefaultUnset + park_and_ride_type: OptionalUnset[str] = NoneToUnsetValue(StringValidator(min_length=0, max_length=256)), DefaultUnset + max_stay: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset + fee_description: OptionalUnset[str] = NoneToUnsetValue(StringValidator(max_length=512)), DefaultUnset + + +@validataclass +class VrnParkAndRideFeaturesInput: + geometry: GeojsonFeatureGeometryInput = DataclassValidator(GeojsonFeatureGeometryInput) + properties: VrnParkAndRidePropertiesInput = DataclassValidator(VrnParkAndRidePropertiesInput) + + def to_static_parking_site_input(self) -> StaticParkingSiteInput: + return StaticParkingSiteInput( + uid=str(self.properties.original_uid), + name=self.properties.name if self.properties.name != '' else 'Fahrrad-Abstellanlagen', + type=self.properties.type.to_parking_site_type(), + description=self.properties.description, + capacity=self.properties.capacity, + has_realtime_data=self.properties.has_realtime_data, + has_lighting=self.properties.has_lighting, + is_covered=self.properties.is_covered, + related_location=self.properties.related_location, + operator_name=self.properties.operator_name, + max_height=self.properties.max_height, + has_fee=self.properties.has_fee, + fee_description=self.properties.fee_description, + capacity_charging=self.properties.capacity_charging, + lat=self.geometry.coordinates[1], + lon=self.geometry.coordinates[0], + static_data_updated_at=datetime.now(timezone.utc) + if self.properties.static_data_updated_at is UnsetValue + else self.properties.static_data_updated_at, + purpose=PurposeType.CAR, + ) diff --git a/src/parkapi_sources/parkapi_sources.py b/src/parkapi_sources/parkapi_sources.py index 86088ca..1f3857a 100644 --- a/src/parkapi_sources/parkapi_sources.py +++ b/src/parkapi_sources/parkapi_sources.py @@ -43,6 +43,9 @@ ReutlingenPushConverter, StuttgartPushConverter, UlmPullConverter, + VrnParkAndRideMultiguidePullConverter, + VrnParkAndRidePullConverter, + VrnParkAndRideSonahPullConverter, VrsBondorfPullConverter, VrsKirchheimPullConverter, VrsNeustadtPullConverter, @@ -93,6 +96,9 @@ class ParkAPISources: ReutlingenBikePushConverter, StuttgartPushConverter, UlmPullConverter, + VrnParkAndRideMultiguidePullConverter, + VrnParkAndRidePullConverter, + VrnParkAndRideSonahPullConverter, VrsBondorfPullConverter, VrsKirchheimPullConverter, VrsNeustadtPullConverter, diff --git a/tests/converters/data/vrn_p_r.json b/tests/converters/data/vrn_p_r.json new file mode 100644 index 0000000..1c87f2f --- /dev/null +++ b/tests/converters/data/vrn_p_r.json @@ -0,0 +1,708 @@ +{ + "features": [ + { + "geometry": { + "type": "Point", + "coordinates": [ + 8.668479999795732, + 49.34205000040286 + ], + "crs": null + }, + "id": 2871, + "type": "Feature", + "properties": { + "fee_description": "[{'string': 'Customers Only: Kostenlos', 'langIso639': 'de'}]", + "max_stay": null, + "capacity_family": null, + "has_lighting": null, + "capacity_woman": 0, + "capacity_disabled": 3, + "created_at": null, + "lon": 8.66848, + "capacity_charging": 0, + "max_height": null, + "capacity_truck": null, + "type": "Parkplatz", + "capacity": 20, + "capacity_bus": null, + "is_covered": "nein", + "public_url": "https://www.parkme.com/lot/153768/st-ilgen-sandhausen-leimen-bw-germany", + "related_location": null, + "original_uid": "2-2-2-153768", + "supervision_type": null, + "photo_url": "https://d13esfgglb25od.cloudfront.net/vrn.jpg", + "modified_at": null, + "lat": 49.34205, + "address": "Leimbachstraße,69181,Leimen", + "static_data_updated_at": 1721389723000, + "vrn_sensor_id": 678, + "park_and_ride_type": "ja", + "realtime_opening_status": "unbekannt", + "has_fee": "nein", + "operator_name": "Sonah", + "name": "P+R St. Ilgen-Sandhausen, Leimbachstr.", + "opening_hours": "[{'string': 'Mo-So: 24 Stunden', 'langIso639': 'de'}]", + "realtime_data_updated_at": null, + "has_realtime_data": "ja", + "source_id": 2871, + "objectid": 2871, + "capacity_carsharing": null + } + }, + { + "geometry": { + "type": "Point", + "coordinates": [ + 8.613580000505587, + 49.70168999996302 + ], + "crs": null + }, + "id": 2872, + "type": "Feature", + "properties": { + "fee_description": "[{'string': 'Mo-So: Kostenlos', 'langIso639': 'de'}]", + "max_stay": null, + "capacity_family": null, + "has_lighting": null, + "capacity_woman": 0, + "capacity_disabled": 2, + "created_at": null, + "lon": 8.61358, + "capacity_charging": 2, + "max_height": null, + "capacity_truck": null, + "type": "Parkplatz", + "capacity": 37, + "capacity_bus": null, + "is_covered": "nein", + "public_url": "https://www.parkme.com/lot/178307/bensheim-auerbach-bensheim-he-germany", + "related_location": null, + "original_uid": "2-2-2-178307", + "supervision_type": null, + "photo_url": "https://d13esfgglb25od.cloudfront.net/vrn.jpg", + "modified_at": null, + "lat": 49.70169, + "address": "Wilhelmstraße 1,64625,Bensheim", + "static_data_updated_at": 1721389723000, + "vrn_sensor_id": null, + "park_and_ride_type": "ja", + "realtime_opening_status": "unbekannt", + "has_fee": "nein", + "operator_name": "MVVEnergie SmartCities", + "name": "P+R Bensheim-Auerbach", + "opening_hours": "[{'string': 'Mo-So: 24 Stunden', 'langIso639': 'de'}]", + "realtime_data_updated_at": null, + "has_realtime_data": "ja", + "source_id": 2872, + "objectid": 2872, + "capacity_carsharing": null + } + }, + { + "geometry": { + "type": "Point", + "coordinates": [ + 8.305570000463035, + 49.37974999964875 + ], + "crs": null + }, + "id": 2873, + "type": "Feature", + "properties": { + "fee_description": "[{'string': 'Mo-So: Kostenlos', 'langIso639': 'de'}]", + "max_stay": null, + "capacity_family": null, + "has_lighting": null, + "capacity_woman": 0, + "capacity_disabled": 2, + "created_at": null, + "lon": 8.30557, + "capacity_charging": 2, + "max_height": null, + "capacity_truck": null, + "type": "Parkplatz", + "capacity": 140, + "capacity_bus": null, + "is_covered": "nein", + "public_url": "https://www.parkme.com/lot/178316/am-bahnhof-bhl-iggelheim-rp-germany", + "related_location": null, + "original_uid": "2-2-2-178316", + "supervision_type": null, + "photo_url": "https://d13esfgglb25od.cloudfront.net/vrn.jpg", + "modified_at": null, + "lat": 49.37975, + "address": "Am Bahnhofspl. 1,67459,Böhl-Iggelheim", + "static_data_updated_at": 1721389723000, + "vrn_sensor_id": 498, + "park_and_ride_type": "ja", + "realtime_opening_status": "unbekannt", + "has_fee": "nein", + "operator_name": "Multiguide", + "name": "P+R Böhl-Iggelheim, Bf Süd", + "opening_hours": "[{'string': 'Mo-So: 24 Stunden', 'langIso639': 'de'}]", + "realtime_data_updated_at": null, + "has_realtime_data": "ja", + "source_id": 2873, + "objectid": 2873, + "capacity_carsharing": null + } + }, + { + "geometry": { + "type": "Point", + "coordinates": [ + 8.305450000503294, + 49.380349999967336 + ], + "crs": null + }, + "id": 2874, + "type": "Feature", + "properties": { + "fee_description": "[{'string': 'Mo-So: Kostenlos', 'langIso639': 'de'}]", + "max_stay": null, + "capacity_family": null, + "has_lighting": null, + "capacity_woman": 0, + "capacity_disabled": 0, + "created_at": null, + "lon": 8.30545, + "capacity_charging": 0, + "max_height": null, + "capacity_truck": null, + "type": "Parkplatz", + "capacity": 61, + "capacity_bus": null, + "is_covered": "nein", + "public_url": "https://www.parkme.com/lot/178317/bahnhofsvorplatz-nord-bhl-iggelheim-rp-germany", + "related_location": null, + "original_uid": "2-2-2-178317", + "supervision_type": null, + "photo_url": "https://d13esfgglb25od.cloudfront.net/vrn.jpg", + "modified_at": null, + "lat": 49.38035, + "address": "Berliner Str. 2A,67459,Böhl-Iggelheim", + "static_data_updated_at": 1721389723000, + "vrn_sensor_id": 499, + "park_and_ride_type": "ja", + "realtime_opening_status": "unbekannt", + "has_fee": "nein", + "operator_name": "Multiguide", + "name": "P+R Böhl-Iggelheim, Bf Nord", + "opening_hours": "[{'string': 'Mo-So: 24 Stunden', 'langIso639': 'de'}]", + "realtime_data_updated_at": null, + "has_realtime_data": "ja", + "source_id": null, + "objectid": 2874, + "capacity_carsharing": null + } + }, + { + "geometry": { + "type": "Point", + "coordinates": [ + 8.253149999448969, + 49.37224999976917 + ], + "crs": null + }, + "id": 2875, + "type": "Feature", + "properties": { + "fee_description": "[{'string': 'Mo-So: Kostenlos', 'langIso639': 'de'}]", + "max_stay": null, + "capacity_family": null, + "has_lighting": null, + "capacity_woman": 0, + "capacity_disabled": 2, + "created_at": null, + "lon": 8.25315, + "capacity_charging": 0, + "max_height": null, + "capacity_truck": null, + "type": "Parkplatz", + "capacity": 190, + "capacity_bus": null, + "is_covered": "nein", + "public_url": "https://www.parkme.com/lot/178381/p-r-haloch-haloch-rp-germany", + "related_location": null, + "original_uid": "2-2-2-178381", + "supervision_type": null, + "photo_url": "https://d13esfgglb25od.cloudfront.net/vrn.jpg", + "modified_at": null, + "lat": 49.37225, + "address": "Lehmgrubenweg 1,67454,Haßloch", + "static_data_updated_at": 1721389723000, + "vrn_sensor_id": 718, + "park_and_ride_type": "ja", + "realtime_opening_status": "unbekannt", + "has_fee": "nein", + "operator_name": "Multiguide", + "name": "P+R Haßloch, Bf Nord", + "opening_hours": "[{'string': 'Mo-So: 24 Stunden', 'langIso639': 'de'}]", + "realtime_data_updated_at": null, + "has_realtime_data": "ja", + "source_id": 2875, + "objectid": 2875, + "capacity_carsharing": null + } + }, + { + "geometry": { + "type": "Point", + "coordinates": [ + 8.25578000008815, + 49.372149999782685 + ], + "crs": null + }, + "id": 2876, + "type": "Feature", + "properties": { + "fee_description": "[{'string': 'Mo-So: Kostenlos', 'langIso639': 'de'}]", + "max_stay": null, + "capacity_family": null, + "has_lighting": null, + "capacity_woman": 0, + "capacity_disabled": 2, + "created_at": null, + "lon": 8.25578, + "capacity_charging": 0, + "max_height": null, + "capacity_truck": null, + "type": "Parkplatz", + "capacity": 114, + "capacity_bus": null, + "is_covered": "nein", + "public_url": "https://www.parkme.com/lot/178382/bahnhofsvorplatz-1", + "related_location": null, + "original_uid": "2-2-2-178382", + "supervision_type": null, + "photo_url": "https://d13esfgglb25od.cloudfront.net/vrn.jpg", + "modified_at": null, + "lat": 49.37215, + "address": "Bahnhofsvorplatz 1,67454,Haßloch", + "static_data_updated_at": 1721389723000, + "vrn_sensor_id": 191, + "park_and_ride_type": "ja", + "realtime_opening_status": "unbekannt", + "has_fee": "nein", + "operator_name": "Multiguide", + "name": "P+R Haßloch, Bf Süd", + "opening_hours": "[{'string': 'Mo-So: 24 Stunden', 'langIso639': 'de'}]", + "realtime_data_updated_at": null, + "has_realtime_data": "ja", + "source_id": null, + "objectid": 2876, + "capacity_carsharing": null + } + }, + { + "geometry": { + "type": "Point", + "coordinates": [ + 8.789859999874407, + 49.39428000018094 + ], + "crs": null + }, + "id": 2877, + "type": "Feature", + "properties": { + "fee_description": "[{'string': 'Mo-So: Kostenlos', 'langIso639': 'de'}]", + "max_stay": null, + "capacity_family": null, + "has_lighting": null, + "capacity_woman": 0, + "capacity_disabled": 1, + "created_at": null, + "lon": 8.78986, + "capacity_charging": 0, + "max_height": null, + "capacity_truck": null, + "type": "Parkplatz", + "capacity": 45, + "capacity_bus": null, + "is_covered": "nein", + "public_url": "https://www.parkme.com/lot/178562/neckargemuend-neckargemnd-bw-germany", + "related_location": null, + "original_uid": "2-2-2-178562", + "supervision_type": null, + "photo_url": "https://d13esfgglb25od.cloudfront.net/vrn.jpg", + "modified_at": null, + "lat": 49.39428, + "address": "Bahnhofstraße 35,69151,Neckargemünd", + "static_data_updated_at": 1721389723000, + "vrn_sensor_id": 676, + "park_and_ride_type": "ja", + "realtime_opening_status": "unbekannt", + "has_fee": "nein", + "operator_name": "Sonah", + "name": "P+R Neckargemünd", + "opening_hours": "[{'string': 'Mo-So: 24 Stunden', 'langIso639': 'de'}]", + "realtime_data_updated_at": null, + "has_realtime_data": "ja", + "source_id": 2877, + "objectid": 2877, + "capacity_carsharing": null + } + }, + { + "geometry": { + "type": "Point", + "coordinates": [ + 8.669239999763356, + 49.34173999989347 + ], + "crs": null + }, + "id": 2878, + "type": "Feature", + "properties": { + "fee_description": "[{'string': 'Customers Only: Kostenlos', 'langIso639': 'de'}]", + "max_stay": null, + "capacity_family": null, + "has_lighting": null, + "capacity_woman": 0, + "capacity_disabled": 6, + "created_at": null, + "lon": 8.66924, + "capacity_charging": 0, + "max_height": null, + "capacity_truck": null, + "type": "Parkplatz", + "capacity": 62, + "capacity_bus": null, + "is_covered": "nein", + "public_url": "https://www.parkme.com/lot/178654/st-ilgen-sandhausen-leimen-bw-germany", + "related_location": null, + "original_uid": "2-2-2-178654", + "supervision_type": null, + "photo_url": "https://d13esfgglb25od.cloudfront.net/vrn.jpg", + "modified_at": null, + "lat": 49.34174, + "address": "Bahnhofstraße 62,69181,Leimen", + "static_data_updated_at": 1721389723000, + "vrn_sensor_id": 679, + "park_and_ride_type": "ja", + "realtime_opening_status": "unbekannt", + "has_fee": "nein", + "operator_name": "Sonah", + "name": "P+R St. Ilgen-Sandhausen, Bf Ost", + "opening_hours": "[{'string': 'Mo-So: 24 Stunden', 'langIso639': 'de'}]", + "realtime_data_updated_at": null, + "has_realtime_data": "ja", + "source_id": 2878, + "objectid": 2878, + "capacity_carsharing": null + } + }, + { + "geometry": { + "type": "Point", + "coordinates": [ + 8.780650000569803, + 49.64836999997582 + ], + "crs": null + }, + "id": 2879, + "type": "Feature", + "properties": { + "fee_description": "[{'string': 'Mo-So: Kostenlos', 'langIso639': 'de'}]", + "max_stay": null, + "capacity_family": null, + "has_lighting": null, + "capacity_woman": 0, + "capacity_disabled": 2, + "created_at": null, + "lon": 8.78065, + "capacity_charging": 0, + "max_height": null, + "capacity_truck": null, + "type": "Parkplatz", + "capacity": 32, + "capacity_bus": null, + "is_covered": "nein", + "public_url": "https://www.parkme.com/lot/179058/frth-odw-bf-p1-frth-he-germany", + "related_location": null, + "original_uid": "2-2-2-179058", + "supervision_type": null, + "photo_url": "https://d13esfgglb25od.cloudfront.net/lot_img/179058/532b13142edc49bfb0a3ebab49035ce9.jpg", + "modified_at": null, + "lat": 49.64837, + "address": "Bahnhofstraße 5,64658,Fürth", + "static_data_updated_at": 1721389723000, + "vrn_sensor_id": 155, + "park_and_ride_type": "ja", + "realtime_opening_status": "unbekannt", + "has_fee": "nein", + "operator_name": "Multiguide", + "name": "P+R Fürth (Odw.) Bf P1", + "opening_hours": "[{'string': 'Mo-So: 24 Stunden', 'langIso639': 'de'}]", + "realtime_data_updated_at": null, + "has_realtime_data": "ja", + "source_id": 2879, + "objectid": 2879, + "capacity_carsharing": null + } + }, + { + "geometry": { + "type": "Point", + "coordinates": [ + 8.753530000273994, + 49.60971999995243 + ], + "crs": null + }, + "id": 2880, + "type": "Feature", + "properties": { + "fee_description": "[{'string': 'Mo-So: Kostenlos', 'langIso639': 'de'}]", + "max_stay": null, + "capacity_family": null, + "has_lighting": null, + "capacity_woman": 0, + "capacity_disabled": 2, + "created_at": null, + "lon": 8.75353, + "capacity_charging": 0, + "max_height": null, + "capacity_truck": null, + "type": "Parkplatz", + "capacity": 28, + "capacity_bus": null, + "is_covered": "nein", + "public_url": "https://www.parkme.com/lot/179073/rimbach-zotzenbach-bf-p1-mrlenbach-he-germany", + "related_location": null, + "original_uid": "2-2-2-179073", + "supervision_type": null, + "photo_url": "https://d13esfgglb25od.cloudfront.net/logo_ivm.png", + "modified_at": null, + "lat": 49.60972, + "address": "Grüne Au,69509,Mörlenbach", + "static_data_updated_at": 1721389723000, + "vrn_sensor_id": null, + "park_and_ride_type": "ja", + "realtime_opening_status": "unbekannt", + "has_fee": "nein", + "operator_name": "MVVEnergie SmartCities", + "name": "P+R Rimbach-Zotzenbach Bf P1", + "opening_hours": "[{'string': 'Mo-So: 24 Stunden', 'langIso639': 'de'}]", + "realtime_data_updated_at": null, + "has_realtime_data": "ja", + "source_id": 2880, + "objectid": 2880, + "capacity_carsharing": null + } + }, + { + "geometry": { + "type": "Point", + "coordinates": [ + 8.537719999545239, + 49.31639999989083 + ], + "crs": null + }, + "id": 2881, + "type": "Feature", + "properties": { + "fee_description": "[{'string': 'Mo-So: Kostenlos', 'langIso639': 'de'}]", + "max_stay": null, + "capacity_family": null, + "has_lighting": null, + "capacity_woman": 0, + "capacity_disabled": 4, + "created_at": null, + "lon": 8.53772, + "capacity_charging": 0, + "max_height": null, + "capacity_truck": null, + "type": "Parkplatz", + "capacity": 288, + "capacity_bus": null, + "is_covered": "nein", + "public_url": "https://www.parkme.com/lot/182421/hockenheim-nord-hockenheim-bw-germany", + "related_location": null, + "original_uid": "2-2-2-182421", + "supervision_type": null, + "photo_url": "https://d13esfgglb25od.cloudfront.net/vrn.jpg", + "modified_at": null, + "lat": 49.3164, + "address": "Eisenbahnstraße 2,68766,Hockenheim", + "static_data_updated_at": 1721389723000, + "vrn_sensor_id": 721, + "park_and_ride_type": "ja", + "realtime_opening_status": "unbekannt", + "has_fee": "nein", + "operator_name": "Multiguide", + "name": "P+R Hockenheim Nord", + "opening_hours": "[{'string': 'Mo-So: 24 Stunden', 'langIso639': 'de'}]", + "realtime_data_updated_at": null, + "has_realtime_data": "ja", + "source_id": 2881, + "objectid": 2881, + "capacity_carsharing": null + } + }, + { + "geometry": { + "type": "Point", + "coordinates": [ + 8.537150000001558, + 49.315610000181195 + ], + "crs": null + }, + "id": 2882, + "type": "Feature", + "properties": { + "fee_description": "[{'string': 'Mo-So: Kostenlos', 'langIso639': 'de'}]", + "max_stay": null, + "capacity_family": null, + "has_lighting": null, + "capacity_woman": 0, + "capacity_disabled": 4, + "created_at": null, + "lon": 8.53715, + "capacity_charging": 0, + "max_height": null, + "capacity_truck": null, + "type": "Parkplatz", + "capacity": 87, + "capacity_bus": null, + "is_covered": "nein", + "public_url": "https://www.parkme.com/lot/182424/hockenheim-sd-hockenheim-bw-germany", + "related_location": null, + "original_uid": "2-2-2-182424", + "supervision_type": null, + "photo_url": "https://d13esfgglb25od.cloudfront.net/vrn.jpg", + "modified_at": null, + "lat": 49.31561, + "address": "Lußheimer Str. 21,68766,Hockenheim", + "static_data_updated_at": 1721389723000, + "vrn_sensor_id": 720, + "park_and_ride_type": "ja", + "realtime_opening_status": "unbekannt", + "has_fee": "nein", + "operator_name": "Multiguide", + "name": "P+R Hockenheim Süd", + "opening_hours": "[{'string': 'Mo-So: 24 Stunden', 'langIso639': 'de'}]", + "realtime_data_updated_at": null, + "has_realtime_data": "ja", + "source_id": 2882, + "objectid": 2882, + "capacity_carsharing": null + } + }, + { + "geometry": { + "type": "Point", + "coordinates": [ + 8.664919999451357, + 49.29114000026921 + ], + "crs": null + }, + "id": 2883, + "type": "Feature", + "properties": { + "fee_description": "[{'string': 'Mo-So: Kostenlos', 'langIso639': 'de'}]", + "max_stay": null, + "capacity_family": null, + "has_lighting": "ja", + "capacity_woman": 0, + "capacity_disabled": 10, + "created_at": null, + "lon": 8.66492, + "capacity_charging": 0, + "max_height": null, + "capacity_truck": null, + "type": "Parkhaus", + "capacity": 263, + "capacity_bus": null, + "is_covered": "ja", + "public_url": "https://www.parkme.com/lot/1222006/p-r-parkhaus-am-bahnhof-wiesloch-walldorf-walldorf-bw-germany", + "related_location": null, + "original_uid": "2-2-2-1222006", + "supervision_type": null, + "photo_url": "https://d13esfgglb25od.cloudfront.net/vrn.jpg", + "modified_at": null, + "lat": 49.29114, + "address": "Staatsbahnhofstraße 14, 69168 Wiesloch", + "static_data_updated_at": 1721389723000, + "vrn_sensor_id": 951, + "park_and_ride_type": "ja", + "realtime_opening_status": "unbekannt", + "has_fee": "ja", + "operator_name": "Multiguide", + "name": "P+R Parkhaus am Bahnhof Wiesloch-Walldorf", + "opening_hours": "[{'string': 'Mo-So: 24 Stunden', 'langIso639': 'de'}]", + "realtime_data_updated_at": null, + "has_realtime_data": "ja", + "source_id": 2883, + "objectid": 2883, + "capacity_carsharing": null + } + }, + { + "geometry": { + "type": "Point", + "coordinates": [ + 8.668783470525582, + 49.343135926233316 + ], + "crs": null + }, + "id": 2884, + "type": "Feature", + "properties": { + "fee_description": "[{'string': 'Customers Only: Kostenlos', 'langIso639': 'de'}]", + "max_stay": null, + "capacity_family": null, + "has_lighting": null, + "capacity_woman": 0, + "capacity_disabled": 3, + "created_at": null, + "lon": 8.66878347, + "capacity_charging": 0, + "max_height": null, + "capacity_truck": null, + "type": "Parkplatz", + "capacity": 52, + "capacity_bus": null, + "is_covered": "nein", + "public_url": "https://www.parkme.com/lot/153768/st-ilgen-sandhausen-leimen-bw-germany", + "related_location": null, + "original_uid": "2-2-2-153768", + "supervision_type": null, + "photo_url": "https://d13esfgglb25od.cloudfront.net/vrn.jpg", + "modified_at": null, + "lat": 49.34313593, + "address": "Am Güterbahnhof,69181,Leimen", + "static_data_updated_at": 1721389723000, + "vrn_sensor_id": 664, + "park_and_ride_type": "ja", + "realtime_opening_status": "unbekannt", + "has_fee": "nein", + "operator_name": "Sonah", + "name": "P+R St. Ilgen-Sandhausen, Am Güterbahnhof", + "opening_hours": "[{'string': 'Mo-So: 24 Stunden', 'langIso639': 'de'}]", + "realtime_data_updated_at": null, + "has_realtime_data": "ja", + "source_id": 2884, + "objectid": 2884, + "capacity_carsharing": null + } + } + ], + "type": "FeatureCollection", + "properties": { + "exceededTransferLimit": false + } +} \ No newline at end of file diff --git a/tests/converters/data/vrn_p_r_multiguide.json b/tests/converters/data/vrn_p_r_multiguide.json new file mode 100644 index 0000000..cf1bcac --- /dev/null +++ b/tests/converters/data/vrn_p_r_multiguide.json @@ -0,0 +1,611 @@ +[ + { + "Id": 108, + "IdParent": -1, + "Name": "VRN", + "Typ": "CarParkGroup", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 782, + "Available": 325, + "DisplayValue": 325, + "Free": 325, + "Occupied": 454, + "Reserved": 0, + "Defect": 3, + "OccupiedPercent": 58.43989769820972, + "Precount": 0, + "Exceeded": 0, + "RealFree": 325, + "RealOccupied": 454, + "RealOccupiedPercent": 41.943734015345271, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 152, + "IdParent": 108, + "Name": "P+R Fürth", + "Typ": "CarPark", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 32, + "Available": 16, + "DisplayValue": 16, + "Free": 16, + "Occupied": 16, + "Reserved": 0, + "Defect": 0, + "OccupiedPercent": 50.0, + "Precount": 0, + "Exceeded": 0, + "RealFree": 16, + "RealOccupied": 16, + "RealOccupiedPercent": 50.0, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 189, + "IdParent": 108, + "Name": "P+R Haßloch", + "Typ": "CarPark", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 304, + "Available": 107, + "DisplayValue": 107, + "Free": 107, + "Occupied": 195, + "Reserved": 0, + "Defect": 2, + "OccupiedPercent": 64.80263157894737, + "Precount": 0, + "Exceeded": 0, + "RealFree": 107, + "RealOccupied": 195, + "RealOccupiedPercent": 35.85526315789474, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 496, + "IdParent": 108, + "Name": "P+R Böhl-Iggelheim", + "Typ": "CarPark", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 217, + "Available": 98, + "DisplayValue": 98, + "Free": 98, + "Occupied": 119, + "Reserved": 0, + "Defect": 0, + "OccupiedPercent": 54.838709677419359, + "Precount": 0, + "Exceeded": 0, + "RealFree": 98, + "RealOccupied": 119, + "RealOccupiedPercent": 45.161290322580648, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 500, + "IdParent": 108, + "Name": "P+R Hockenheim", + "Typ": "CarPark", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 229, + "Available": 104, + "DisplayValue": 104, + "Free": 104, + "Occupied": 124, + "Reserved": 0, + "Defect": 1, + "OccupiedPercent": 54.585152838427945, + "Precount": 0, + "Exceeded": 0, + "RealFree": 104, + "RealOccupied": 124, + "RealOccupiedPercent": 45.851528384279469, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 951, + "IdParent": 108, + "Name": "P+R Wiesloch", + "Typ": "CarPark", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 0, + "Available": 0, + "DisplayValue": 0, + "Free": 0, + "Occupied": 0, + "Reserved": 0, + "Defect": 0, + "OccupiedPercent": 0.0, + "Precount": 0, + "Exceeded": 0, + "RealFree": 0, + "RealOccupied": 0, + "RealOccupiedPercent": 0.0, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 153, + "IdParent": 152, + "Name": "P_R Bahnhofstr. ", + "Typ": "AreaGroup", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 32, + "Available": 16, + "DisplayValue": 16, + "Free": 16, + "Occupied": 16, + "Reserved": 0, + "Defect": 0, + "OccupiedPercent": 50.0, + "Precount": 0, + "Exceeded": 0, + "RealFree": 16, + "RealOccupied": 16, + "RealOccupiedPercent": 50.0, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 190, + "IdParent": 189, + "Name": "P_R Haßloch", + "Typ": "AreaGroup", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 304, + "Available": 107, + "DisplayValue": 107, + "Free": 107, + "Occupied": 195, + "Reserved": 0, + "Defect": 2, + "OccupiedPercent": 64.80263157894737, + "Precount": 0, + "Exceeded": 0, + "RealFree": 107, + "RealOccupied": 195, + "RealOccupiedPercent": 35.85526315789474, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 497, + "IdParent": 496, + "Name": "P_R Böhl-Iggelheim", + "Typ": "AreaGroup", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 217, + "Available": 98, + "DisplayValue": 98, + "Free": 98, + "Occupied": 119, + "Reserved": 0, + "Defect": 0, + "OccupiedPercent": 54.838709677419359, + "Precount": 0, + "Exceeded": 0, + "RealFree": 98, + "RealOccupied": 119, + "RealOccupiedPercent": 45.161290322580648, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 719, + "IdParent": 500, + "Name": "P_R Hockenheim", + "Typ": "AreaGroup", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 229, + "Available": 104, + "DisplayValue": 104, + "Free": 104, + "Occupied": 124, + "Reserved": 0, + "Defect": 1, + "OccupiedPercent": 54.585152838427945, + "Precount": 0, + "Exceeded": 0, + "RealFree": 104, + "RealOccupied": 124, + "RealOccupiedPercent": 45.851528384279469, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 952, + "IdParent": 951, + "Name": "PH Ebene 0", + "Typ": "AreaGroup", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 134, + "Available": 0, + "DisplayValue": 0, + "Free": 0, + "Occupied": 0, + "Reserved": 0, + "Defect": 134, + "OccupiedPercent": 100.0, + "Precount": 0, + "Exceeded": 0, + "RealFree": 0, + "RealOccupied": 0, + "RealOccupiedPercent": 100.0, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 953, + "IdParent": 951, + "Name": "PH Ebene 1", + "Typ": "AreaGroup", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 130, + "Available": 0, + "DisplayValue": 0, + "Free": 0, + "Occupied": 0, + "Reserved": 0, + "Defect": 130, + "OccupiedPercent": 100.0, + "Precount": 0, + "Exceeded": 0, + "RealFree": 0, + "RealOccupied": 0, + "RealOccupiedPercent": 100.0, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 954, + "IdParent": 952, + "Name": "A001", + "Typ": "Area", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 134, + "Available": 0, + "DisplayValue": 0, + "Free": 0, + "Occupied": 0, + "Reserved": 0, + "Defect": 134, + "OccupiedPercent": 100.0, + "Precount": 0, + "Exceeded": 0, + "RealFree": 0, + "RealOccupied": 0, + "RealOccupiedPercent": 100.0, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 955, + "IdParent": 953, + "Name": "A101", + "Typ": "Area", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 130, + "Available": 0, + "DisplayValue": 0, + "Free": 0, + "Occupied": 0, + "Reserved": 0, + "Defect": 130, + "OccupiedPercent": 100.0, + "Precount": 0, + "Exceeded": 0, + "RealFree": 0, + "RealOccupied": 0, + "RealOccupiedPercent": 100.0, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 191, + "IdParent": 190, + "Name": "P_R_h Süd", + "Typ": "Area", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 114, + "Available": 8, + "DisplayValue": 8, + "Free": 8, + "Occupied": 104, + "Reserved": 0, + "Defect": 2, + "OccupiedPercent": 92.982456140350877, + "Precount": 0, + "Exceeded": 0, + "RealFree": 8, + "RealOccupied": 104, + "RealOccupiedPercent": 8.7719298245614112, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 720, + "IdParent": 719, + "Name": " P_R hck Süd", + "Typ": "Area", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 52, + "Available": 39, + "DisplayValue": 39, + "Free": 39, + "Occupied": 13, + "Reserved": 0, + "Defect": 0, + "OccupiedPercent": 25.0, + "Precount": 0, + "Exceeded": 0, + "RealFree": 39, + "RealOccupied": 13, + "RealOccupiedPercent": 75.0, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 721, + "IdParent": 719, + "Name": " P_R hck Nord", + "Typ": "Area", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 177, + "Available": 65, + "DisplayValue": 65, + "Free": 65, + "Occupied": 111, + "Reserved": 0, + "Defect": 1, + "OccupiedPercent": 63.27683615819209, + "Precount": 0, + "Exceeded": 0, + "RealFree": 65, + "RealOccupied": 111, + "RealOccupiedPercent": 37.288135593220339, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 498, + "IdParent": 497, + "Name": "P_R_bh Süd", + "Typ": "Area", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 156, + "Available": 52, + "DisplayValue": 52, + "Free": 52, + "Occupied": 104, + "Reserved": 0, + "Defect": 0, + "OccupiedPercent": 66.666666666666671, + "Precount": 0, + "Exceeded": 0, + "RealFree": 52, + "RealOccupied": 104, + "RealOccupiedPercent": 33.333333333333343, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 718, + "IdParent": 190, + "Name": "P_R_h Nord", + "Typ": "Area", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 190, + "Available": 99, + "DisplayValue": 99, + "Free": 99, + "Occupied": 91, + "Reserved": 0, + "Defect": 0, + "OccupiedPercent": 47.894736842105267, + "Precount": 0, + "Exceeded": 0, + "RealFree": 99, + "RealOccupied": 91, + "RealOccupiedPercent": 52.10526315789474, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 499, + "IdParent": 497, + "Name": "P_R_bh Nord", + "Typ": "Area", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 61, + "Available": 46, + "DisplayValue": 46, + "Free": 46, + "Occupied": 15, + "Reserved": 0, + "Defect": 0, + "OccupiedPercent": 24.590163934426229, + "Precount": 0, + "Exceeded": 0, + "RealFree": 46, + "RealOccupied": 15, + "RealOccupiedPercent": 75.409836065573771, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 155, + "IdParent": 153, + "Name": "P_R_b", + "Typ": "Area", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 32, + "Available": 16, + "DisplayValue": 16, + "Free": 16, + "Occupied": 16, + "Reserved": 0, + "Defect": 0, + "OccupiedPercent": 50.0, + "Precount": 0, + "Exceeded": 0, + "RealFree": 16, + "RealOccupied": 16, + "RealOccupiedPercent": 50.0, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + } +] \ No newline at end of file diff --git a/tests/converters/data/vrn_p_r_sonah.json b/tests/converters/data/vrn_p_r_sonah.json new file mode 100644 index 0000000..e93c89c --- /dev/null +++ b/tests/converters/data/vrn_p_r_sonah.json @@ -0,0 +1,305 @@ +[ + { + "LocationID": 664, + "Name": "Am Güterbahnhof", + "Type": "STATIC", + "FreeParking": 40.0, + "OccupiedParking": 0.0, + "TotalParking": 40.0, + "ParentLocations": [ + "api/v3/rest/json/locations?filter=677&exclude=SubLocations&select=[0]" + ], + "Areas": [ + "api/v3/rest/json/areas?filter=3897&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3898&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3899&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3900&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3901&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3902&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3903&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3904&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3905&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3906&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3907&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3908&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3909&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3910&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3911&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3912&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3913&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3914&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3915&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3916&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3917&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3918&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3919&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3920&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3921&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3922&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3923&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3924&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3925&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3926&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3927&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3928&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3929&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3930&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3931&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3932&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3933&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3934&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3935&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3936&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3937&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3938&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3939&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3940&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3941&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3942&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3943&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3944&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3945&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3946&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3947&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3948&exclude=Location&select=[0]" + ], + "SubLocations": [], + "Positions": { + "Center": { + "Lat": 49.34319583651544, + "Long": 8.668797095979574 + }, + "Navigation": { + "Lat": null, + "Long": null + } + }, + "Geometries": [] + }, + { + "LocationID": 676, + "Name": "Neckargemünd", + "Type": "STATIC", + "FreeParking": 17.0, + "OccupiedParking": 26.0, + "TotalParking": 43.0, + "ParentLocations": [], + "Areas": [ + "api/v3/rest/json/areas?filter=3949&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3950&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3951&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3952&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3953&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3954&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3955&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3956&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3957&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3958&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3959&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3960&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3961&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3962&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3963&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3964&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3965&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3966&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3967&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3968&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3969&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3970&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3971&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3972&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3973&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3974&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3975&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3976&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3977&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3978&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3979&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3980&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3981&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3982&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3983&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3984&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3985&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3986&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3987&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3988&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3989&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3990&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4465&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4466&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4467&exclude=Location&select=[0]" + ], + "SubLocations": [], + "Positions": { + "Center": { + "Lat": 49.39428030857759, + "Long": 8.789651524948898 + }, + "Navigation": { + "Lat": null, + "Long": null + } + }, + "Geometries": [] + }, + { + "LocationID": 677, + "Name": "St. Ilgen - Sandhausen", + "Type": "GROUP", + "FreeParking": 101.0, + "OccupiedParking": 21.0, + "TotalParking": 122.0, + "ParentLocations": [], + "Areas": [], + "SubLocations": [ + "api/v3/rest/json/locations?filter=664&exclude=ParentLocations&select=[0]", + "api/v3/rest/json/locations?filter=678&exclude=ParentLocations&select=[0]", + "api/v3/rest/json/locations?filter=679&exclude=ParentLocations&select=[0]" + ], + "Positions": { + "Center": { + "Lat": null, + "Long": null + }, + "Navigation": { + "Lat": null, + "Long": null + } + }, + "Geometries": [] + }, + { + "LocationID": 678, + "Name": "Leimbachstraße", + "Type": "STATIC", + "FreeParking": 12.0, + "OccupiedParking": 8.0, + "TotalParking": 20.0, + "ParentLocations": [ + "api/v3/rest/json/locations?filter=677&exclude=SubLocations&select=[0]" + ], + "Areas": [ + "api/v3/rest/json/areas?filter=3991&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3992&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3993&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3994&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3995&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3996&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3997&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3998&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3999&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4000&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4001&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4002&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4003&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4004&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4005&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4006&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4007&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4008&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4009&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4010&exclude=Location&select=[0]" + ], + "SubLocations": [], + "Positions": { + "Center": { + "Lat": 49.34209431064309, + "Long": 8.668326465412974 + }, + "Navigation": { + "Lat": null, + "Long": null + } + }, + "Geometries": [] + }, + { + "LocationID": 679, + "Name": "Bahnhofstraße", + "Type": "STATIC", + "FreeParking": 49.0, + "OccupiedParking": 13.0, + "TotalParking": 62.0, + "ParentLocations": [ + "api/v3/rest/json/locations?filter=677&exclude=SubLocations&select=[0]" + ], + "Areas": [ + "api/v3/rest/json/areas?filter=4011&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4012&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4013&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4014&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4015&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4016&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4017&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4018&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4019&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4020&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4021&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4022&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4023&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4024&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4025&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4026&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4027&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4028&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4029&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4030&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4031&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4032&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4033&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4034&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4035&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4036&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4037&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4038&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4039&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4040&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4041&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4042&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4043&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4044&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4045&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4046&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4047&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4048&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4049&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4050&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4051&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4052&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4053&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4054&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4055&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4056&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4057&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4058&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4059&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4060&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4061&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4062&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4063&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4064&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4065&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4066&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4067&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4068&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4069&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4070&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4071&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4072&exclude=Location&select=[0]" + ], + "SubLocations": [], + "Positions": { + "Center": { + "Lat": 49.341713800212275, + "Long": 8.669230901378217 + }, + "Navigation": { + "Lat": null, + "Long": null + } + }, + "Geometries": [] + } +] \ No newline at end of file diff --git a/tests/converters/vrn_p_r_test.py b/tests/converters/vrn_p_r_test.py new file mode 100644 index 0000000..fac8ce9 --- /dev/null +++ b/tests/converters/vrn_p_r_test.py @@ -0,0 +1,114 @@ +""" +Copyright 2024 binary butterfly GmbH +Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt. +""" + +from pathlib import Path +from unittest.mock import Mock + +import pytest +from parkapi_sources.converters import VrnParkAndRideMultiguidePullConverter, VrnParkAndRidePullConverter, VrnParkAndRideSonahPullConverter +from requests_mock import Mocker + +from tests.converters.helper import validate_realtime_parking_site_inputs, validate_static_parking_site_inputs + + +@pytest.fixture +def vrn_p_r_config_helper(mocked_config_helper: Mock): + config = {} + mocked_config_helper.get.side_effect = lambda key, default=None: config.get(key, default) + return mocked_config_helper + + +@pytest.fixture +def vrn_p_r_pull_converter(vrn_p_r_config_helper: Mock) -> VrnParkAndRidePullConverter: + return VrnParkAndRidePullConverter(config_helper=vrn_p_r_config_helper) + + +@pytest.fixture +def vrn_multiguide_config_helper(mocked_config_helper: Mock): + config = { + 'PARK_API_VRN_P_R_MULTIGUIDE_USERNAME': '0152d634-9e16-46c0-bfef-20c0b623eaa3', + 'PARK_API_VRN_P_R_MULTIGUIDE_PASSWORD': 'eaf7a00c-d0e1-4464-a9dc-f8ef4d01f2cc', + } + mocked_config_helper.get.side_effect = lambda key, default=None: config.get(key, default) + return mocked_config_helper + + +@pytest.fixture +def vrn_multiguide_pull_converter(vrn_multiguide_config_helper: Mock) -> VrnParkAndRideMultiguidePullConverter: + return VrnParkAndRideMultiguidePullConverter(config_helper=vrn_multiguide_config_helper) + + +@pytest.fixture +def vrn_sonah_config_helper(mocked_config_helper: Mock): + config = { + 'PARK_API_VRN_P_R_SONAH_BEARER_TOKEN': '0152d634-9e16-46c0-bfef-20c0b623eaa3', + } + mocked_config_helper.get.side_effect = lambda key, default=None: config.get(key, default) + return mocked_config_helper + + +@pytest.fixture +def vrn_sonah_pull_converter(vrn_sonah_config_helper: Mock) -> VrnParkAndRideSonahPullConverter: + return VrnParkAndRideSonahPullConverter(config_helper=vrn_sonah_config_helper) + + +class VrnParkAndRidePullConverterTest: + @staticmethod + def test_get_static_parking_sites(vrn_p_r_pull_converter: VrnParkAndRidePullConverter, requests_mock: Mocker): + json_path = Path(Path(__file__).parent, 'data', 'vrn_p_r.json') + with json_path.open() as json_file: + json_data = json_file.read() + + requests_mock.get( + 'https://spatial.vrn.de/data/rest/services/Hosted/p_r_parkapi_static/FeatureServer/5/query?where=objectid%3E0&outFields=*&returnGeometry=true&f=geojson', + text=json_data, + ) + + static_parking_site_inputs, import_parking_site_exceptions = vrn_p_r_pull_converter.get_static_parking_sites() + + assert len(static_parking_site_inputs) == 14 + assert len(import_parking_site_exceptions) == 0 + + validate_static_parking_site_inputs(static_parking_site_inputs) + + +class VrnParkAndRideMultiguidePullConverterTest: + @staticmethod + def test_get_realtime_parking_sites(vrn_multiguide_pull_converter: VrnParkAndRideMultiguidePullConverter, requests_mock: Mocker): + json_path = Path(Path(__file__).parent, 'data', 'vrn_p_r_multiguide.json') + with json_path.open() as json_file: + json_data = json_file.read() + + requests_mock.get( + 'https://vrn.multiguide.info/api/area', + text=json_data, + ) + + realtime_parking_site_inputs, import_parking_site_exceptions = vrn_multiguide_pull_converter.get_realtime_parking_sites() + + assert len(realtime_parking_site_inputs) == 21 + assert len(import_parking_site_exceptions) == 0 + + validate_realtime_parking_site_inputs(realtime_parking_site_inputs) + + +class VrnParkAndRideSonahPullConverterTest: + @staticmethod + def test_get_realtime_parking_sites(vrn_sonah_pull_converter: VrnParkAndRideSonahPullConverter, requests_mock: Mocker): + json_path = Path(Path(__file__).parent, 'data', 'vrn_p_r_sonah.json') + with json_path.open() as json_file: + json_data = json_file.read() + + requests_mock.get( + 'https://vrnm.dyndns.sonah.xyz/api/v3/rest/json/locations', + text=json_data, + ) + + realtime_parking_site_inputs, import_parking_site_exceptions = vrn_sonah_pull_converter.get_realtime_parking_sites() + + assert len(realtime_parking_site_inputs) == 5 + assert len(import_parking_site_exceptions) == 0 + + validate_realtime_parking_site_inputs(realtime_parking_site_inputs) From 92497a506f6e9900a5ff22989775e64c162c8a69 Mon Sep 17 00:00:00 2001 From: Abdullahi Fatola Date: Tue, 24 Sep 2024 17:25:28 +0200 Subject: [PATCH 2/3] Updated the converter and models --- README.md | 2 - src/parkapi_sources/converters/__init__.py | 2 +- .../converters/vrn_p_r/__init__.py | 3 +- .../converters/vrn_p_r/converter.py | 198 +++++++++++------- .../converters/vrn_p_r/models.py | 116 +++++++++- .../converters/vrn_p_r/vrn_converter.py | 85 -------- .../converters/vrn_p_r/vrn_models.py | 111 ---------- src/parkapi_sources/parkapi_sources.py | 4 - tests/converters/vrn_p_r_test.py | 95 ++++----- 9 files changed, 280 insertions(+), 336 deletions(-) delete mode 100644 src/parkapi_sources/converters/vrn_p_r/vrn_converter.py delete mode 100644 src/parkapi_sources/converters/vrn_p_r/vrn_models.py diff --git a/README.md b/README.md index a340944..acb938e 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,6 @@ We support following data sources: | Stadt Stuttgart | car | push (json) | `stuttgart` | yes | | Stadt Ulm | car | pull | `ulm` | yes | | Verkehrsverbund Rhein-Neckar GmbH: P+R Parkplätze | car | pull | `vrn_p_r` | yes | -| Verkehrsverbund Rhein-Neckar GmbH: Multiguide API - P+R Parkplätze | car | pull | `vrn_p_r_multiguide` | yes | -| Verkehrsverbund Rhein-Neckar GmbH: Sonah API - P+R Parkplätze | car | pull | `vrn_p_r_sonah` | yes | | Verband Region Stuttgart: Bondorf | car | pull | `vrs_bondorf` | yes | | Verband Region Stuttgart: Kirchheim | car | pull | `vrs_kirchheim` | yes | | Verband Region Stuttgart: Neustadt | car | pull | `vrs_neustadt` | yes | diff --git a/src/parkapi_sources/converters/__init__.py b/src/parkapi_sources/converters/__init__.py index d6b30e5..4dbc438 100644 --- a/src/parkapi_sources/converters/__init__.py +++ b/src/parkapi_sources/converters/__init__.py @@ -39,6 +39,6 @@ from .reutlingen_bike import ReutlingenBikePushConverter from .stuttgart import StuttgartPushConverter from .ulm import UlmPullConverter -from .vrn_p_r import VrnParkAndRideMultiguidePullConverter, VrnParkAndRidePullConverter, VrnParkAndRideSonahPullConverter +from .vrn_p_r import VrnParkAndRidePullConverter from .vrs import VrsBondorfPullConverter, VrsKirchheimPullConverter, VrsNeustadtPullConverter, VrsVaihingenPullConverter from .vrs_p_r import VrsParkAndRidePushConverter diff --git a/src/parkapi_sources/converters/vrn_p_r/__init__.py b/src/parkapi_sources/converters/vrn_p_r/__init__.py index da0d944..fc5a00f 100644 --- a/src/parkapi_sources/converters/vrn_p_r/__init__.py +++ b/src/parkapi_sources/converters/vrn_p_r/__init__.py @@ -3,5 +3,4 @@ Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt. """ -from .converter import VrnParkAndRideMultiguidePullConverter, VrnParkAndRideSonahPullConverter -from .vrn_converter import VrnParkAndRidePullConverter +from .converter import VrnParkAndRidePullConverter diff --git a/src/parkapi_sources/converters/vrn_p_r/converter.py b/src/parkapi_sources/converters/vrn_p_r/converter.py index f8b4428..bab3e51 100644 --- a/src/parkapi_sources/converters/vrn_p_r/converter.py +++ b/src/parkapi_sources/converters/vrn_p_r/converter.py @@ -3,140 +3,194 @@ Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt. """ +from abc import ABC + import requests from validataclass.exceptions import ValidationError from validataclass.validators import AnythingValidator, DataclassValidator, ListValidator +from parkapi_sources.converters.base_converter.pull import GeojsonInput, PullConverter from parkapi_sources.exceptions import ImportParkingSiteException, ImportSourceException from parkapi_sources.models import RealtimeParkingSiteInput, SourceInfo, StaticParkingSiteInput -from .models import VrnParkAndRideMultiguideInput, VrnParkAndRideSonahInput -from .vrn_converter import VrnParkAndRidePullConverter +from .models import VrnParkAndRideFeaturesInput, VrnParkAndRideMultiguideInput, VrnParkAndRideSonahInput -class VrnParkAndRideMultiguidePullConverter(VrnParkAndRidePullConverter): +class VrnParkAndRidePullConverter(PullConverter, ABC): list_validator = ListValidator(AnythingValidator(allowed_types=[dict])) + geojson_validator = DataclassValidator(GeojsonInput) + vrn_p_r_feature_validator = DataclassValidator(VrnParkAndRideFeaturesInput) vrn_multiguide_validator = DataclassValidator(VrnParkAndRideMultiguideInput) + vrn_sonah_validator = DataclassValidator(VrnParkAndRideSonahInput) source_info = SourceInfo( - uid='vrn_p_r_multiguide', - name='Verkehrsverbund Rhein-Neckar GmbH: Multiguide API - P+R Parkplätze', - source_url='https://vrn.multiguide.info/api/area', - has_realtime_data=True, # ATM it's impossible to get realtime data due rate limit restrictions + uid='vrn_p_r', + name='Verkehrsverbund Rhein-Neckar GmbH - P+R Parkplätze', + public_url='https://www.vrn.de/opendata/datasets/pr-parkplaetze-mit-vrn-parksensorik', + timezone='Europe/Berlin', + has_realtime_data=True, ) - def get_static_parking_sites(self) -> tuple[list[StaticParkingSiteInput], list[ImportParkingSiteException]]: - return self._get_raw_static_parking_sites() - - def get_realtime_parking_sites(self) -> tuple[list[RealtimeParkingSiteInput], list[ImportParkingSiteException]]: - realtime_parking_site_inputs: list[RealtimeParkingSiteInput] = [] - - realtime_multiguide_inputs, import_parking_site_exceptions = self._get_raw_realtime_parking_sites() - - for realtime_multiguide_input in realtime_multiguide_inputs: - realtime_parking_site_inputs.append(realtime_multiguide_input.to_realtime_parking_site_input()) - - return realtime_parking_site_inputs, import_parking_site_exceptions - - def _get_raw_realtime_parking_sites(self) -> tuple[list[VrnParkAndRideMultiguideInput], list[ImportParkingSiteException]]: - vrn_multiguide_inputs: list[VrnParkAndRideMultiguideInput] = [] + def _get_feature_inputs(self) -> tuple[list[VrnParkAndRideFeaturesInput], list[ImportParkingSiteException]]: + feature_inputs: list[VrnParkAndRideFeaturesInput] = [] import_parking_site_exceptions: list[ImportParkingSiteException] = [] response = requests.get( - url=self.source_info.source_url, - auth=( - self.config_helper.get('PARK_API_VRN_P_R_MULTIGUIDE_USERNAME'), - self.config_helper.get('PARK_API_VRN_P_R_MULTIGUIDE_PASSWORD'), - ), + url='https://spatial.vrn.de/data/rest/services/Hosted/p_r_parkapi_static/FeatureServer/5/query?where=objectid%3E0&outFields=*&returnGeometry=true&f=geojson', timeout=30, ) response_data = response.json() + try: - input_dicts = self.list_validator.validate(response_data) + geojson_input = self.geojson_validator.validate(response_data) except ValidationError as e: raise ImportSourceException( source_uid=self.source_info.uid, message=f'Invalid Input at source {self.source_info.uid}: {e.to_dict()}, data: {response_data}', ) from e - for input_dict in input_dicts: + for feature_dict in geojson_input.features: + if self._should_ignore_dataset(feature_dict): + continue + try: - vrn_multiguide_input = self.vrn_multiguide_validator.validate(input_dict) + feature_input = self.vrn_p_r_feature_validator.validate(feature_dict) except ValidationError as e: import_parking_site_exceptions.append( ImportParkingSiteException( source_uid=self.source_info.uid, - parking_site_uid=input_dict.get('Id'), - message=f'Invalid data at uid {input_dict.get("Id")}: {e.to_dict()}, ' f'data: {input_dict}', + parking_site_uid=feature_dict.get('properties', {}).get('id'), + message=f'Invalid data at uid {feature_dict.get("properties", {}).get("id")}: ' + f'{e.to_dict()}, data: {feature_dict}', ), ) continue - vrn_multiguide_inputs.append(vrn_multiguide_input) - - return vrn_multiguide_inputs, import_parking_site_exceptions + feature_inputs.append(feature_input) + return feature_inputs, import_parking_site_exceptions -class VrnParkAndRideSonahPullConverter(VrnParkAndRidePullConverter): - list_validator = ListValidator(AnythingValidator(allowed_types=[dict])) - vrn_sonah_validator = DataclassValidator(VrnParkAndRideSonahInput) + def _should_ignore_dataset(self, feature_dict: dict) -> bool: + if self.config_helper.get('PARK_API_VRN_P_R_IGNORE_MISSING_CAPACITIES'): + return feature_dict.get('properties', {}).get('capacity') is None - source_info = SourceInfo( - uid='vrn_p_r_sonah', - name='Verkehrsverbund Rhein-Neckar GmbH: Sonah API - P+R Parkplätze', - source_url='https://vrnm.dyndns.sonah.xyz/api/v3/rest/json/locations', - has_realtime_data=True, # ATM it's impossible to get realtime data due rate limit restrictions - ) + return False def get_static_parking_sites(self) -> tuple[list[StaticParkingSiteInput], list[ImportParkingSiteException]]: - return self._get_raw_static_parking_sites() + feature_inputs, import_parking_site_exceptions = self._get_feature_inputs() + static_parking_site_inputs: list[StaticParkingSiteInput] = [] - def get_realtime_parking_sites(self) -> tuple[list[RealtimeParkingSiteInput], list[ImportParkingSiteException]]: - realtime_parking_site_inputs: list[RealtimeParkingSiteInput] = [] + realtime_vrn_p_r_inputs, import_realtime_parking_site_exceptions = self._get_raw_realtime_parking_sites() + import_parking_site_exceptions += import_realtime_parking_site_exceptions - realtime_sonah_inputs, import_parking_site_exceptions = self._get_raw_realtime_parking_sites() + static_parking_site_inputs_by_uid: dict[str, StaticParkingSiteInput] = {} + for feature_input in feature_inputs: + static_parking_site_inputs_by_uid[str(feature_input.properties.vrn_sensor_id)] = feature_input.to_static_parking_site_input() - for realtime_sonah_input in realtime_sonah_inputs: - realtime_parking_site_inputs.append(realtime_sonah_input.to_realtime_parking_site_input()) + for realtime_vrn_p_r_input in realtime_vrn_p_r_inputs: + # If the realtime group_uid is not known in our static data: ignore the static data + parking_site_uid = str(realtime_vrn_p_r_input.uid) + if parking_site_uid in static_parking_site_inputs_by_uid: + # Update static data with only parking places having realtime data + static_parking_site_inputs.append(static_parking_site_inputs_by_uid[parking_site_uid]) - return realtime_parking_site_inputs, import_parking_site_exceptions + return static_parking_site_inputs, import_parking_site_exceptions - def _get_raw_realtime_parking_sites(self) -> tuple[list[VrnParkAndRideSonahInput], list[ImportParkingSiteException]]: - vrn_sonah_inputs: list[VrnParkAndRideSonahInput] = [] + def _get_raw_realtime_parking_sites(self) -> tuple[list[RealtimeParkingSiteInput], list[ImportParkingSiteException]]: + realtime_parking_site_inputs: list[RealtimeParkingSiteInput] = [] import_parking_site_exceptions: list[ImportParkingSiteException] = [] - headers: dict[str, str] = { - 'Accept': 'application/json', - 'Authorization': self.config_helper.get('PARK_API_VRN_P_R_SONAH_BEARER_TOKEN'), - } + multiguide_response_data = self._request_vrn_multiguide() + sonah_response_data = self._request_vrn_sonah() - response = requests.get( - self.source_info.source_url, - headers=headers, - timeout=30, - ) - response_data = response.json() try: - input_dicts = self.list_validator.validate(response_data) + multiguide_parking_site_dicts = self.list_validator.validate(multiguide_response_data) except ValidationError as e: raise ImportSourceException( source_uid=self.source_info.uid, - message=f'Invalid Input at source {self.source_info.uid}: {e.to_dict()}, data: {response_data}', + message=f'Invalid Input at source {self.source_info.uid}: {e.to_dict()}, data: {multiguide_response_data}', ) from e - for input_dict in input_dicts: + for multiguide_parking_site_dict in multiguide_parking_site_dicts: try: - vrn_sonah_input = self.vrn_sonah_validator.validate(input_dict) + realtime_parking_site_inputs.append( + self.vrn_multiguide_validator.validate(multiguide_parking_site_dict).to_realtime_parking_site_input() + ) except ValidationError as e: import_parking_site_exceptions.append( ImportParkingSiteException( source_uid=self.source_info.uid, - parking_site_uid=input_dict.get('LocationID'), - message=f'Invalid data at uid {input_dict.get("LocationID")}: {e.to_dict()}, ' f'data: {input_dict}', + parking_site_uid=multiguide_parking_site_dict.get('Id'), + message=f'validation error for {multiguide_parking_site_dict}: {e.to_dict()}', ), ) - continue - vrn_sonah_inputs.append(vrn_sonah_input) + try: + sonah_parking_site_dicts = self.list_validator.validate(sonah_response_data) + except ValidationError as e: + raise ImportSourceException( + source_uid=self.source_info.uid, + message=f'Invalid Input at source {self.source_info.uid}: {e.to_dict()}, data: {sonah_response_data}', + ) from e + + for sonah_parking_site_dict in sonah_parking_site_dicts: + try: + realtime_parking_site_inputs.append( + self.vrn_sonah_validator.validate(sonah_parking_site_dict).to_realtime_parking_site_input() + ) + except ValidationError as e: + import_parking_site_exceptions.append( + ImportParkingSiteException( + source_uid=self.source_info.uid, + parking_site_uid=sonah_parking_site_dict.get('LocationID'), + message=f'validation error for {sonah_parking_site_dict}: {e.to_dict()}', + ), + ) + + return realtime_parking_site_inputs, import_parking_site_exceptions + + def get_realtime_parking_sites(self) -> tuple[list[RealtimeParkingSiteInput], list[ImportParkingSiteException]]: + static_parking_site_inputs, import_parking_site_exceptions = self.get_static_parking_sites() + realtime_parking_site_inputs: list[RealtimeParkingSiteInput] = [] + + realtime_vrn_p_r_inputs, import_parking_site_exceptions = self._get_raw_realtime_parking_sites() + static_parking_site_inputs_by_uid: dict[str, StaticParkingSiteInput] = {} + + for static_parking_site_input in static_parking_site_inputs: + static_parking_site_inputs_by_uid[static_parking_site_input.group_uid] = static_parking_site_input + + for realtime_vrn_p_r_input in realtime_vrn_p_r_inputs: + # If the realtime group_uid is not known in our static data: ignore the realtime data + parking_site_uid = str(realtime_vrn_p_r_input.uid) + if parking_site_uid in static_parking_site_inputs_by_uid: + # Update realtime data with only parking places having static data + realtime_vrn_p_r_input.uid = static_parking_site_inputs_by_uid[parking_site_uid].uid + realtime_parking_site_inputs.append(realtime_vrn_p_r_input) + + return realtime_parking_site_inputs, import_parking_site_exceptions + + def _request_vrn_multiguide(self) -> list[dict]: + response = requests.get( + url='https://vrn.multiguide.info/api/area', + auth=( + self.config_helper.get('PARK_API_VRN_P_R_MULTIGUIDE_API_USERNAME'), + self.config_helper.get('PARK_API_VRN_P_R_MULTIGUIDE_API_PASSWORD'), + ), + timeout=30, + ) + + return response.json() + + def _request_vrn_sonah(self) -> list[dict]: + headers: dict[str, str] = { + 'Accept': 'application/json', + 'Authorization': self.config_helper.get('PARK_API_VRN_P_R_SONAH_API_BEARER_TOKEN'), + } + + response = requests.get( + url='https://vrnm.dyndns.sonah.xyz/api/v3/rest/json/locations', + headers=headers, + timeout=30, + ) - return vrn_sonah_inputs, import_parking_site_exceptions + return response.json() diff --git a/src/parkapi_sources/converters/vrn_p_r/models.py b/src/parkapi_sources/converters/vrn_p_r/models.py index 26a7aa2..a22848f 100644 --- a/src/parkapi_sources/converters/vrn_p_r/models.py +++ b/src/parkapi_sources/converters/vrn_p_r/models.py @@ -3,13 +3,117 @@ Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt. """ -from datetime import datetime, timezone +from datetime import date, datetime, timezone +from decimal import Decimal +from enum import Enum -from validataclass.dataclasses import validataclass +from validataclass.dataclasses import DefaultUnset, ValidataclassMixin, validataclass from validataclass.exceptions import ValidationError -from validataclass.validators import IntegerValidator, NumericValidator, StringValidator +from validataclass.helpers import OptionalUnset, UnsetValue +from validataclass.validators import ( + DataclassValidator, + EnumValidator, + IntegerValidator, + NoneToUnsetValue, + NumericValidator, + StringValidator, + UrlValidator, +) -from parkapi_sources.models import RealtimeParkingSiteInput +from parkapi_sources.converters.base_converter.pull import GeojsonFeatureGeometryInput +from parkapi_sources.models import RealtimeParkingSiteInput, StaticParkingSiteInput +from parkapi_sources.models.enums import ParkingSiteType, PurposeType +from parkapi_sources.validators import MappedBooleanValidator, ParsedDateValidator, TimestampDateTimeValidator + + +class VrnParkAndRideType(Enum): + CAR_PARK = 'Parkhaus' + OFF_STREET_PARKING_GROUND = 'Parkplatz' + + def to_parking_site_type(self) -> ParkingSiteType: + return { + self.CAR_PARK: ParkingSiteType.CAR_PARK, + self.OFF_STREET_PARKING_GROUND: ParkingSiteType.OFF_STREET_PARKING_GROUND, + }.get(self) + + +@validataclass +class VrnParkAndRidePropertiesOpeningHoursInput: + string: str = StringValidator(min_length=1, max_length=256) + langIso639: str = StringValidator(min_length=1, max_length=256) + + +@validataclass +class VrnParkAndRidePropertiesInput(ValidataclassMixin): + original_uid: str = StringValidator(min_length=1, max_length=256) + name: str = StringValidator(min_length=0, max_length=256) + type: OptionalUnset[VrnParkAndRideType] = NoneToUnsetValue(EnumValidator(VrnParkAndRideType)), DefaultUnset + public_url: OptionalUnset[str] = NoneToUnsetValue(UrlValidator(max_length=4096)), DefaultUnset + photo_url: OptionalUnset[str] = NoneToUnsetValue(UrlValidator(max_length=4096)), DefaultUnset + lat: OptionalUnset[Decimal] = NumericValidator() + lon: OptionalUnset[Decimal] = NumericValidator() + address: OptionalUnset[str] = NoneToUnsetValue(StringValidator(min_length=0, max_length=256)), DefaultUnset + description: OptionalUnset[str] = NoneToUnsetValue(StringValidator(max_length=512)), DefaultUnset + operator_name: OptionalUnset[str] = NoneToUnsetValue(StringValidator(min_length=0, max_length=256)), DefaultUnset + capacity: int = IntegerValidator(min_value=0) + capacity_charging: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset + capacity_family: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset + capacity_woman: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset + capacity_bus: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset + capacity_truck: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset + capacity_carsharing: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset + capacity_disabled: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset + max_height: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset + has_realtime_data: OptionalUnset[bool] = NoneToUnsetValue(MappedBooleanValidator(mapping={'ja': True, 'nein': False})), DefaultUnset + vrn_sensor_id: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset + realtime_opening_status: OptionalUnset[str] = NoneToUnsetValue(StringValidator(min_length=0, max_length=256)), DefaultUnset + created_at: OptionalUnset[date] = NoneToUnsetValue(ParsedDateValidator(date_format='%Y-%m-%d')), DefaultUnset + static_data_updated_at: OptionalUnset[datetime] = ( + NoneToUnsetValue(TimestampDateTimeValidator(allow_strings=True, divisor=1000)), + DefaultUnset, + ) + has_lighting: OptionalUnset[bool] = NoneToUnsetValue(MappedBooleanValidator(mapping={'ja': True, 'nein': False})), DefaultUnset + has_fee: OptionalUnset[bool] = NoneToUnsetValue(MappedBooleanValidator(mapping={'ja': True, 'nein': False})), DefaultUnset + is_covered: OptionalUnset[bool] = NoneToUnsetValue(MappedBooleanValidator(mapping={'ja': True, 'nein': False})), DefaultUnset + related_location: OptionalUnset[str] = NoneToUnsetValue(StringValidator(min_length=0, max_length=256)), DefaultUnset + opening_hours: OptionalUnset[str] = NoneToUnsetValue(StringValidator(min_length=0, max_length=256)), DefaultUnset + park_and_ride_type: OptionalUnset[str] = NoneToUnsetValue(StringValidator(min_length=0, max_length=256)), DefaultUnset + max_stay: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset + fee_description: OptionalUnset[str] = NoneToUnsetValue(StringValidator(max_length=512)), DefaultUnset + + +@validataclass +class VrnParkAndRideFeaturesInput: + geometry: GeojsonFeatureGeometryInput = DataclassValidator(GeojsonFeatureGeometryInput) + properties: VrnParkAndRidePropertiesInput = DataclassValidator(VrnParkAndRidePropertiesInput) + + def to_static_parking_site_input(self) -> StaticParkingSiteInput: + return StaticParkingSiteInput( + uid=str(self.properties.original_uid), + group_uid=str(self.properties.vrn_sensor_id), + name=self.properties.name if self.properties.name != '' else 'Fahrrad-Abstellanlagen', + type=self.properties.type.to_parking_site_type(), + description=self.properties.description, + capacity=self.properties.capacity, + has_realtime_data=self.properties.has_realtime_data, + has_lighting=self.properties.has_lighting, + is_covered=self.properties.is_covered, + related_location=self.properties.related_location, + operator_name=self.properties.operator_name, + max_height=self.properties.max_height, + has_fee=self.properties.has_fee, + fee_description=self.properties.fee_description, + capacity_charging=self.properties.capacity_charging, + lat=self.geometry.coordinates[1], + lon=self.geometry.coordinates[0], + static_data_updated_at=datetime.now(timezone.utc) + if self.properties.static_data_updated_at is UnsetValue + else self.properties.static_data_updated_at, + purpose=PurposeType.CAR, + opening_hours='24/7' + if 'Mo-So: 24 Stunden' in self.properties.opening_hours or 'Mo-So: Kostenlos' in self.properties.opening_hours + else UnsetValue, + ) @validataclass @@ -29,7 +133,7 @@ def __post_init__(self): def to_realtime_parking_site_input(self) -> RealtimeParkingSiteInput: return RealtimeParkingSiteInput( - uid=self.Name, + uid=str(self.Id), realtime_capacity=self.Constructed, realtime_free_capacity=self.Free, realtime_data_updated_at=datetime.now(timezone.utc), @@ -50,7 +154,7 @@ def __post_init__(self): def to_realtime_parking_site_input(self) -> RealtimeParkingSiteInput: return RealtimeParkingSiteInput( - uid=self.Name, + uid=str(self.LocationID), realtime_capacity=int(self.TotalParking), realtime_free_capacity=int(self.FreeParking), realtime_data_updated_at=datetime.now(timezone.utc), diff --git a/src/parkapi_sources/converters/vrn_p_r/vrn_converter.py b/src/parkapi_sources/converters/vrn_p_r/vrn_converter.py deleted file mode 100644 index 9e2c5e1..0000000 --- a/src/parkapi_sources/converters/vrn_p_r/vrn_converter.py +++ /dev/null @@ -1,85 +0,0 @@ -""" -Copyright 2024 binary butterfly GmbH -Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt. -""" - -import requests -from validataclass.exceptions import ValidationError -from validataclass.validators import DataclassValidator - -from parkapi_sources.converters.base_converter.pull import GeojsonInput, PullConverter -from parkapi_sources.exceptions import ImportParkingSiteException, ImportSourceException -from parkapi_sources.models import RealtimeParkingSiteInput, SourceInfo, StaticParkingSiteInput - -from .vrn_models import VrnParkAndRideFeaturesInput - - -class VrnParkAndRidePullConverter(PullConverter): - geojson_validator = DataclassValidator(GeojsonInput) - vrn_p_r_feature_validator = DataclassValidator(VrnParkAndRideFeaturesInput) - - source_info = SourceInfo( - uid='vrn_p_r', - name='Verkehrsverbund Rhein-Neckar GmbH - P+R Parkplätze', - public_url='https://www.vrn.de/opendata/datasets/pr-parkplaetze-mit-vrn-parksensorik', - source_url='https://spatial.vrn.de/data/rest/services/Hosted/p_r_parkapi_static/FeatureServer/5/query?where=objectid%3E0&outFields=*&returnGeometry=true&f=geojson', - timezone='Europe/Berlin', - has_realtime_data=False, - ) - - def _get_feature_inputs(self) -> tuple[list[VrnParkAndRideFeaturesInput], list[ImportParkingSiteException]]: - feature_inputs: list[VrnParkAndRideFeaturesInput] = [] - import_parking_site_exceptions: list[ImportParkingSiteException] = [] - - response = requests.get(self.source_info.source_url, timeout=30) - response_data = response.json() - - try: - geojson_input = self.geojson_validator.validate(response_data) - except ValidationError as e: - raise ImportSourceException( - source_uid=self.source_info.uid, - message=f'Invalid Input at source {self.source_info.uid}: {e.to_dict()}, data: {response_data}', - ) from e - - for feature_dict in geojson_input.features: - if self._should_ignore_dataset(feature_dict): - continue - - try: - feature_input = self.vrn_p_r_feature_validator.validate(feature_dict) - except ValidationError as e: - import_parking_site_exceptions.append( - ImportParkingSiteException( - source_uid=self.source_info.uid, - parking_site_uid=feature_dict.get('properties', {}).get('id'), - message=f'Invalid data at uid {feature_dict.get("properties", {}).get("id")}: ' - f'{e.to_dict()}, data: {feature_dict}', - ), - ) - continue - - feature_inputs.append(feature_input) - - return feature_inputs, import_parking_site_exceptions - - def _should_ignore_dataset(self, feature_dict: dict) -> bool: - if self.config_helper.get('PARK_API_Vrn_P_R_IGNORE_MISSING_CAPACITIES'): - return feature_dict.get('properties', {}).get('capacity') is None - - return False - - def get_static_parking_sites(self) -> tuple[list[StaticParkingSiteInput], list[ImportParkingSiteException]]: - return self._get_raw_static_parking_sites() - - def _get_raw_static_parking_sites(self) -> tuple[list[StaticParkingSiteInput], list[ImportParkingSiteException]]: - feature_inputs, import_parking_site_exceptions = self._get_feature_inputs() - - static_parking_site_inputs: list[StaticParkingSiteInput] = [] - for feature_input in feature_inputs: - static_parking_site_inputs.append(feature_input.to_static_parking_site_input()) - - return static_parking_site_inputs, import_parking_site_exceptions - - def get_realtime_parking_sites(self) -> tuple[list[RealtimeParkingSiteInput], list[ImportParkingSiteException]]: - return [], [] diff --git a/src/parkapi_sources/converters/vrn_p_r/vrn_models.py b/src/parkapi_sources/converters/vrn_p_r/vrn_models.py deleted file mode 100644 index d010b80..0000000 --- a/src/parkapi_sources/converters/vrn_p_r/vrn_models.py +++ /dev/null @@ -1,111 +0,0 @@ -""" -Copyright 2024 binary butterfly GmbH -Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt. -""" - -from datetime import date, datetime, timezone -from decimal import Decimal -from enum import Enum - -from validataclass.dataclasses import DefaultUnset, ValidataclassMixin, validataclass -from validataclass.helpers import OptionalUnset, UnsetValue -from validataclass.validators import ( - DataclassValidator, - EnumValidator, - IntegerValidator, - NoneToUnsetValue, - NumericValidator, - StringValidator, - UrlValidator, -) - -from parkapi_sources.converters.base_converter.pull import GeojsonFeatureGeometryInput -from parkapi_sources.models import StaticParkingSiteInput -from parkapi_sources.models.enums import ParkingSiteType, PurposeType -from parkapi_sources.validators import MappedBooleanValidator, ParsedDateValidator, TimestampDateTimeValidator - - -class VrnParkAndRideType(Enum): - CAR_PARK = 'Parkhaus' - OFF_STREET_PARKING_GROUND = 'Parkplatz' - - def to_parking_site_type(self) -> ParkingSiteType: - return { - self.CAR_PARK: ParkingSiteType.CAR_PARK, - self.OFF_STREET_PARKING_GROUND: ParkingSiteType.OFF_STREET_PARKING_GROUND, - }.get(self) - - -@validataclass -class VrnParkAndRidePropertiesInput(ValidataclassMixin): - original_uid: str = StringValidator(min_length=1, max_length=256) - name: str = StringValidator(min_length=0, max_length=256) - type: OptionalUnset[VrnParkAndRideType] = NoneToUnsetValue(EnumValidator(VrnParkAndRideType)), DefaultUnset - public_url: OptionalUnset[str] = NoneToUnsetValue(UrlValidator(max_length=4096)), DefaultUnset - photo_url: OptionalUnset[str] = NoneToUnsetValue(UrlValidator(max_length=4096)), DefaultUnset - lat: OptionalUnset[Decimal] = NumericValidator() - lon: OptionalUnset[Decimal] = NumericValidator() - address: OptionalUnset[str] = NoneToUnsetValue(StringValidator(min_length=0, max_length=256)), DefaultUnset - description: OptionalUnset[str] = NoneToUnsetValue(StringValidator(max_length=512)), DefaultUnset - operator_name: OptionalUnset[str] = NoneToUnsetValue(StringValidator(min_length=0, max_length=256)), DefaultUnset - - capacity: int = IntegerValidator(min_value=0) - capacity_charging: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset - capacity_family: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset - capacity_woman: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset - capacity_bus: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset - capacity_truck: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset - capacity_carsharing: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset - capacity_disabled: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset - max_height: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset - # supervision_type: OptionalUnset[HerrenbergBikeSupervisionType] = ( - # NoneToUnsetValue(EnumValidator(HerrenbergBikeSupervisionType)), - # DefaultUnset, - # ) - has_realtime_data: OptionalUnset[bool] = NoneToUnsetValue(MappedBooleanValidator(mapping={'ja': True, 'nein': False})), DefaultUnset - vrn_sensor_id: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset - realtime_opening_status: OptionalUnset[str] = NoneToUnsetValue(StringValidator(min_length=0, max_length=256)), DefaultUnset - created_at: OptionalUnset[date] = NoneToUnsetValue(ParsedDateValidator(date_format='%Y-%m-%d')), DefaultUnset - # modified_at: OptionalUnset[date] = NoneToUnsetValue(ParsedDateValidator(date_format='%Y-%m-%d')), DefaultUnset - static_data_updated_at: OptionalUnset[datetime] = ( - NoneToUnsetValue(TimestampDateTimeValidator(allow_strings=True, divisor=1000)), - DefaultUnset, - ) - has_lighting: OptionalUnset[bool] = NoneToUnsetValue(MappedBooleanValidator(mapping={'ja': True, 'nein': False})), DefaultUnset - has_fee: OptionalUnset[bool] = NoneToUnsetValue(MappedBooleanValidator(mapping={'ja': True, 'nein': False})), DefaultUnset - is_covered: OptionalUnset[bool] = NoneToUnsetValue(MappedBooleanValidator(mapping={'ja': True, 'nein': False})), DefaultUnset - related_location: OptionalUnset[str] = NoneToUnsetValue(StringValidator(min_length=0, max_length=256)), DefaultUnset - opening_hours: OptionalUnset[str] = NoneToUnsetValue(StringValidator(min_length=0, max_length=256)), DefaultUnset - park_and_ride_type: OptionalUnset[str] = NoneToUnsetValue(StringValidator(min_length=0, max_length=256)), DefaultUnset - max_stay: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset - fee_description: OptionalUnset[str] = NoneToUnsetValue(StringValidator(max_length=512)), DefaultUnset - - -@validataclass -class VrnParkAndRideFeaturesInput: - geometry: GeojsonFeatureGeometryInput = DataclassValidator(GeojsonFeatureGeometryInput) - properties: VrnParkAndRidePropertiesInput = DataclassValidator(VrnParkAndRidePropertiesInput) - - def to_static_parking_site_input(self) -> StaticParkingSiteInput: - return StaticParkingSiteInput( - uid=str(self.properties.original_uid), - name=self.properties.name if self.properties.name != '' else 'Fahrrad-Abstellanlagen', - type=self.properties.type.to_parking_site_type(), - description=self.properties.description, - capacity=self.properties.capacity, - has_realtime_data=self.properties.has_realtime_data, - has_lighting=self.properties.has_lighting, - is_covered=self.properties.is_covered, - related_location=self.properties.related_location, - operator_name=self.properties.operator_name, - max_height=self.properties.max_height, - has_fee=self.properties.has_fee, - fee_description=self.properties.fee_description, - capacity_charging=self.properties.capacity_charging, - lat=self.geometry.coordinates[1], - lon=self.geometry.coordinates[0], - static_data_updated_at=datetime.now(timezone.utc) - if self.properties.static_data_updated_at is UnsetValue - else self.properties.static_data_updated_at, - purpose=PurposeType.CAR, - ) diff --git a/src/parkapi_sources/parkapi_sources.py b/src/parkapi_sources/parkapi_sources.py index 1f3857a..e073c36 100644 --- a/src/parkapi_sources/parkapi_sources.py +++ b/src/parkapi_sources/parkapi_sources.py @@ -43,9 +43,7 @@ ReutlingenPushConverter, StuttgartPushConverter, UlmPullConverter, - VrnParkAndRideMultiguidePullConverter, VrnParkAndRidePullConverter, - VrnParkAndRideSonahPullConverter, VrsBondorfPullConverter, VrsKirchheimPullConverter, VrsNeustadtPullConverter, @@ -96,9 +94,7 @@ class ParkAPISources: ReutlingenBikePushConverter, StuttgartPushConverter, UlmPullConverter, - VrnParkAndRideMultiguidePullConverter, VrnParkAndRidePullConverter, - VrnParkAndRideSonahPullConverter, VrsBondorfPullConverter, VrsKirchheimPullConverter, VrsNeustadtPullConverter, diff --git a/tests/converters/vrn_p_r_test.py b/tests/converters/vrn_p_r_test.py index fac8ce9..6272828 100644 --- a/tests/converters/vrn_p_r_test.py +++ b/tests/converters/vrn_p_r_test.py @@ -7,7 +7,7 @@ from unittest.mock import Mock import pytest -from parkapi_sources.converters import VrnParkAndRideMultiguidePullConverter, VrnParkAndRidePullConverter, VrnParkAndRideSonahPullConverter +from parkapi_sources.converters import VrnParkAndRidePullConverter from requests_mock import Mocker from tests.converters.helper import validate_realtime_parking_site_inputs, validate_static_parking_site_inputs @@ -15,46 +15,21 @@ @pytest.fixture def vrn_p_r_config_helper(mocked_config_helper: Mock): - config = {} - mocked_config_helper.get.side_effect = lambda key, default=None: config.get(key, default) - return mocked_config_helper - - -@pytest.fixture -def vrn_p_r_pull_converter(vrn_p_r_config_helper: Mock) -> VrnParkAndRidePullConverter: - return VrnParkAndRidePullConverter(config_helper=vrn_p_r_config_helper) - - -@pytest.fixture -def vrn_multiguide_config_helper(mocked_config_helper: Mock): - config = { - 'PARK_API_VRN_P_R_MULTIGUIDE_USERNAME': '0152d634-9e16-46c0-bfef-20c0b623eaa3', - 'PARK_API_VRN_P_R_MULTIGUIDE_PASSWORD': 'eaf7a00c-d0e1-4464-a9dc-f8ef4d01f2cc', - } - mocked_config_helper.get.side_effect = lambda key, default=None: config.get(key, default) - return mocked_config_helper - - -@pytest.fixture -def vrn_multiguide_pull_converter(vrn_multiguide_config_helper: Mock) -> VrnParkAndRideMultiguidePullConverter: - return VrnParkAndRideMultiguidePullConverter(config_helper=vrn_multiguide_config_helper) - - -@pytest.fixture -def vrn_sonah_config_helper(mocked_config_helper: Mock): config = { - 'PARK_API_VRN_P_R_SONAH_BEARER_TOKEN': '0152d634-9e16-46c0-bfef-20c0b623eaa3', + 'PARK_API_VRN_P_R_MULTIGUIDE_API_USERNAME': '0152d634-9e16-46c0-bfef-20c0b623eaa3', + 'PARK_API_VRN_P_R_MULTIGUIDE_API_PASSWORD': 'eaf7a00c-d0e1-4464-a9dc-f8ef4d01f2cc', + 'PARK_API_VRN_P_R_SONAH_API_BEARER_TOKEN': '0152d634-9e16-46c0-bfef-20c0b623eaa3', } mocked_config_helper.get.side_effect = lambda key, default=None: config.get(key, default) return mocked_config_helper @pytest.fixture -def vrn_sonah_pull_converter(vrn_sonah_config_helper: Mock) -> VrnParkAndRideSonahPullConverter: - return VrnParkAndRideSonahPullConverter(config_helper=vrn_sonah_config_helper) +def vrn_p_r_pull_converter(vrn_p_r_config_helper: Mock) -> VrnParkAndRidePullConverter: + return VrnParkAndRidePullConverter(config_helper=vrn_p_r_config_helper) -class VrnParkAndRidePullConverterTest: +class VrnParkAndRideBaseConverterTest: @staticmethod def test_get_static_parking_sites(vrn_p_r_pull_converter: VrnParkAndRidePullConverter, requests_mock: Mocker): json_path = Path(Path(__file__).parent, 'data', 'vrn_p_r.json') @@ -66,49 +41,63 @@ def test_get_static_parking_sites(vrn_p_r_pull_converter: VrnParkAndRidePullConv text=json_data, ) + multiguide_json_path = Path(Path(__file__).parent, 'data', 'vrn_p_r_multiguide.json') + with multiguide_json_path.open() as json_file: + multiguide_json_data = json_file.read() + + requests_mock.get( + 'https://vrn.multiguide.info/api/area', + text=multiguide_json_data, + ) + + sonah_json_path = Path(Path(__file__).parent, 'data', 'vrn_p_r_sonah.json') + with sonah_json_path.open() as json_file: + sonah_json_data = json_file.read() + + requests_mock.get( + 'https://vrnm.dyndns.sonah.xyz/api/v3/rest/json/locations', + text=sonah_json_data, + ) + static_parking_site_inputs, import_parking_site_exceptions = vrn_p_r_pull_converter.get_static_parking_sites() - assert len(static_parking_site_inputs) == 14 + assert len(static_parking_site_inputs) == 12 assert len(import_parking_site_exceptions) == 0 validate_static_parking_site_inputs(static_parking_site_inputs) - -class VrnParkAndRideMultiguidePullConverterTest: @staticmethod - def test_get_realtime_parking_sites(vrn_multiguide_pull_converter: VrnParkAndRideMultiguidePullConverter, requests_mock: Mocker): - json_path = Path(Path(__file__).parent, 'data', 'vrn_p_r_multiguide.json') + def test_get_realtime_parking_sites(vrn_p_r_pull_converter: VrnParkAndRidePullConverter, requests_mock: Mocker): + json_path = Path(Path(__file__).parent, 'data', 'vrn_p_r.json') with json_path.open() as json_file: json_data = json_file.read() requests_mock.get( - 'https://vrn.multiguide.info/api/area', + 'https://spatial.vrn.de/data/rest/services/Hosted/p_r_parkapi_static/FeatureServer/5/query?where=objectid%3E0&outFields=*&returnGeometry=true&f=geojson', text=json_data, ) - realtime_parking_site_inputs, import_parking_site_exceptions = vrn_multiguide_pull_converter.get_realtime_parking_sites() - - assert len(realtime_parking_site_inputs) == 21 - assert len(import_parking_site_exceptions) == 0 - - validate_realtime_parking_site_inputs(realtime_parking_site_inputs) + multiguide_json_path = Path(Path(__file__).parent, 'data', 'vrn_p_r_multiguide.json') + with multiguide_json_path.open() as json_file: + multiguide_json_data = json_file.read() + requests_mock.get( + 'https://vrn.multiguide.info/api/area', + text=multiguide_json_data, + ) -class VrnParkAndRideSonahPullConverterTest: - @staticmethod - def test_get_realtime_parking_sites(vrn_sonah_pull_converter: VrnParkAndRideSonahPullConverter, requests_mock: Mocker): - json_path = Path(Path(__file__).parent, 'data', 'vrn_p_r_sonah.json') - with json_path.open() as json_file: - json_data = json_file.read() + sonah_json_path = Path(Path(__file__).parent, 'data', 'vrn_p_r_sonah.json') + with sonah_json_path.open() as json_file: + sonah_json_data = json_file.read() requests_mock.get( 'https://vrnm.dyndns.sonah.xyz/api/v3/rest/json/locations', - text=json_data, + text=sonah_json_data, ) - realtime_parking_site_inputs, import_parking_site_exceptions = vrn_sonah_pull_converter.get_realtime_parking_sites() + realtime_parking_site_inputs, import_parking_site_exceptions = vrn_p_r_pull_converter.get_realtime_parking_sites() - assert len(realtime_parking_site_inputs) == 5 + assert len(realtime_parking_site_inputs) == 12 assert len(import_parking_site_exceptions) == 0 validate_realtime_parking_site_inputs(realtime_parking_site_inputs) From 9b274aaba92f0f85eb867bd42727cb9ba6d11df1 Mon Sep 17 00:00:00 2001 From: Abdullahi Fatola Date: Wed, 25 Sep 2024 11:19:15 +0200 Subject: [PATCH 3/3] Refined the tests --- .../converters/vrn_p_r/converter.py | 1 - tests/converters/data/vrn_p_r_multiguide.json | 248 +++++++++--------- tests/converters/data/vrn_p_r_sonah.json | 20 +- 3 files changed, 134 insertions(+), 135 deletions(-) diff --git a/src/parkapi_sources/converters/vrn_p_r/converter.py b/src/parkapi_sources/converters/vrn_p_r/converter.py index bab3e51..85bbb35 100644 --- a/src/parkapi_sources/converters/vrn_p_r/converter.py +++ b/src/parkapi_sources/converters/vrn_p_r/converter.py @@ -186,7 +186,6 @@ def _request_vrn_sonah(self) -> list[dict]: 'Accept': 'application/json', 'Authorization': self.config_helper.get('PARK_API_VRN_P_R_SONAH_API_BEARER_TOKEN'), } - response = requests.get( url='https://vrnm.dyndns.sonah.xyz/api/v3/rest/json/locations', headers=headers, diff --git a/tests/converters/data/vrn_p_r_multiguide.json b/tests/converters/data/vrn_p_r_multiguide.json index cf1bcac..d6c98d2 100644 --- a/tests/converters/data/vrn_p_r_multiguide.json +++ b/tests/converters/data/vrn_p_r_multiguide.json @@ -8,18 +8,18 @@ "Buffer": 0, "Hysteresis": 0, "Constructed": 782, - "Available": 325, - "DisplayValue": 325, - "Free": 325, - "Occupied": 454, + "Available": 324, + "DisplayValue": 324, + "Free": 324, + "Occupied": 456, "Reserved": 0, - "Defect": 3, - "OccupiedPercent": 58.43989769820972, + "Defect": 2, + "OccupiedPercent": 58.567774936061376, "Precount": 0, "Exceeded": 0, - "RealFree": 325, - "RealOccupied": 454, - "RealOccupiedPercent": 41.943734015345271, + "RealFree": 324, + "RealOccupied": 456, + "RealOccupiedPercent": 41.687979539641937, "Comment": null, "Location_Name": null, "Location_Street": null, @@ -37,18 +37,18 @@ "Buffer": 0, "Hysteresis": 0, "Constructed": 32, - "Available": 16, - "DisplayValue": 16, - "Free": 16, - "Occupied": 16, + "Available": 15, + "DisplayValue": 15, + "Free": 15, + "Occupied": 17, "Reserved": 0, "Defect": 0, - "OccupiedPercent": 50.0, + "OccupiedPercent": 53.125, "Precount": 0, "Exceeded": 0, - "RealFree": 16, - "RealOccupied": 16, - "RealOccupiedPercent": 50.0, + "RealFree": 15, + "RealOccupied": 17, + "RealOccupiedPercent": 46.875, "Comment": null, "Location_Name": null, "Location_Street": null, @@ -66,18 +66,18 @@ "Buffer": 0, "Hysteresis": 0, "Constructed": 304, - "Available": 107, - "DisplayValue": 107, - "Free": 107, - "Occupied": 195, + "Available": 122, + "DisplayValue": 122, + "Free": 122, + "Occupied": 181, "Reserved": 0, - "Defect": 2, - "OccupiedPercent": 64.80263157894737, + "Defect": 1, + "OccupiedPercent": 59.868421052631575, "Precount": 0, "Exceeded": 0, - "RealFree": 107, - "RealOccupied": 195, - "RealOccupiedPercent": 35.85526315789474, + "RealFree": 122, + "RealOccupied": 181, + "RealOccupiedPercent": 40.460526315789465, "Comment": null, "Location_Name": null, "Location_Street": null, @@ -95,18 +95,18 @@ "Buffer": 0, "Hysteresis": 0, "Constructed": 217, - "Available": 98, - "DisplayValue": 98, - "Free": 98, - "Occupied": 119, + "Available": 67, + "DisplayValue": 67, + "Free": 67, + "Occupied": 150, "Reserved": 0, "Defect": 0, - "OccupiedPercent": 54.838709677419359, + "OccupiedPercent": 69.124423963133637, "Precount": 0, "Exceeded": 0, - "RealFree": 98, - "RealOccupied": 119, - "RealOccupiedPercent": 45.161290322580648, + "RealFree": 67, + "RealOccupied": 150, + "RealOccupiedPercent": 30.875576036866363, "Comment": null, "Location_Name": null, "Location_Street": null, @@ -124,18 +124,18 @@ "Buffer": 0, "Hysteresis": 0, "Constructed": 229, - "Available": 104, - "DisplayValue": 104, - "Free": 104, - "Occupied": 124, + "Available": 120, + "DisplayValue": 120, + "Free": 120, + "Occupied": 108, "Reserved": 0, "Defect": 1, - "OccupiedPercent": 54.585152838427945, + "OccupiedPercent": 47.598253275109172, "Precount": 0, "Exceeded": 0, - "RealFree": 104, - "RealOccupied": 124, - "RealOccupiedPercent": 45.851528384279469, + "RealFree": 120, + "RealOccupied": 108, + "RealOccupiedPercent": 52.838427947598255, "Comment": null, "Location_Name": null, "Location_Street": null, @@ -182,18 +182,18 @@ "Buffer": 0, "Hysteresis": 0, "Constructed": 32, - "Available": 16, - "DisplayValue": 16, - "Free": 16, - "Occupied": 16, + "Available": 15, + "DisplayValue": 15, + "Free": 15, + "Occupied": 17, "Reserved": 0, "Defect": 0, - "OccupiedPercent": 50.0, + "OccupiedPercent": 53.125, "Precount": 0, "Exceeded": 0, - "RealFree": 16, - "RealOccupied": 16, - "RealOccupiedPercent": 50.0, + "RealFree": 15, + "RealOccupied": 17, + "RealOccupiedPercent": 46.875, "Comment": null, "Location_Name": null, "Location_Street": null, @@ -211,18 +211,18 @@ "Buffer": 0, "Hysteresis": 0, "Constructed": 304, - "Available": 107, - "DisplayValue": 107, - "Free": 107, - "Occupied": 195, + "Available": 122, + "DisplayValue": 122, + "Free": 122, + "Occupied": 181, "Reserved": 0, - "Defect": 2, - "OccupiedPercent": 64.80263157894737, + "Defect": 1, + "OccupiedPercent": 59.868421052631575, "Precount": 0, "Exceeded": 0, - "RealFree": 107, - "RealOccupied": 195, - "RealOccupiedPercent": 35.85526315789474, + "RealFree": 122, + "RealOccupied": 181, + "RealOccupiedPercent": 40.460526315789465, "Comment": null, "Location_Name": null, "Location_Street": null, @@ -240,18 +240,18 @@ "Buffer": 0, "Hysteresis": 0, "Constructed": 217, - "Available": 98, - "DisplayValue": 98, - "Free": 98, - "Occupied": 119, + "Available": 67, + "DisplayValue": 67, + "Free": 67, + "Occupied": 150, "Reserved": 0, "Defect": 0, - "OccupiedPercent": 54.838709677419359, + "OccupiedPercent": 69.124423963133637, "Precount": 0, "Exceeded": 0, - "RealFree": 98, - "RealOccupied": 119, - "RealOccupiedPercent": 45.161290322580648, + "RealFree": 67, + "RealOccupied": 150, + "RealOccupiedPercent": 30.875576036866363, "Comment": null, "Location_Name": null, "Location_Street": null, @@ -269,18 +269,18 @@ "Buffer": 0, "Hysteresis": 0, "Constructed": 229, - "Available": 104, - "DisplayValue": 104, - "Free": 104, - "Occupied": 124, + "Available": 120, + "DisplayValue": 120, + "Free": 120, + "Occupied": 108, "Reserved": 0, "Defect": 1, - "OccupiedPercent": 54.585152838427945, + "OccupiedPercent": 47.598253275109172, "Precount": 0, "Exceeded": 0, - "RealFree": 104, - "RealOccupied": 124, - "RealOccupiedPercent": 45.851528384279469, + "RealFree": 120, + "RealOccupied": 108, + "RealOccupiedPercent": 52.838427947598255, "Comment": null, "Location_Name": null, "Location_Street": null, @@ -414,18 +414,18 @@ "Buffer": 0, "Hysteresis": 0, "Constructed": 114, - "Available": 8, - "DisplayValue": 8, - "Free": 8, - "Occupied": 104, + "Available": 23, + "DisplayValue": 23, + "Free": 23, + "Occupied": 90, "Reserved": 0, - "Defect": 2, - "OccupiedPercent": 92.982456140350877, + "Defect": 1, + "OccupiedPercent": 79.824561403508767, "Precount": 0, "Exceeded": 0, - "RealFree": 8, - "RealOccupied": 104, - "RealOccupiedPercent": 8.7719298245614112, + "RealFree": 23, + "RealOccupied": 90, + "RealOccupiedPercent": 21.05263157894737, "Comment": null, "Location_Name": null, "Location_Street": null, @@ -443,18 +443,18 @@ "Buffer": 0, "Hysteresis": 0, "Constructed": 52, - "Available": 39, - "DisplayValue": 39, - "Free": 39, - "Occupied": 13, + "Available": 40, + "DisplayValue": 40, + "Free": 40, + "Occupied": 12, "Reserved": 0, "Defect": 0, - "OccupiedPercent": 25.0, + "OccupiedPercent": 23.076923076923066, "Precount": 0, "Exceeded": 0, - "RealFree": 39, - "RealOccupied": 13, - "RealOccupiedPercent": 75.0, + "RealFree": 40, + "RealOccupied": 12, + "RealOccupiedPercent": 76.92307692307692, "Comment": null, "Location_Name": null, "Location_Street": null, @@ -472,18 +472,18 @@ "Buffer": 0, "Hysteresis": 0, "Constructed": 177, - "Available": 65, - "DisplayValue": 65, - "Free": 65, - "Occupied": 111, + "Available": 80, + "DisplayValue": 80, + "Free": 80, + "Occupied": 96, "Reserved": 0, "Defect": 1, - "OccupiedPercent": 63.27683615819209, + "OccupiedPercent": 54.802259887005647, "Precount": 0, "Exceeded": 0, - "RealFree": 65, - "RealOccupied": 111, - "RealOccupiedPercent": 37.288135593220339, + "RealFree": 80, + "RealOccupied": 96, + "RealOccupiedPercent": 45.762711864406782, "Comment": null, "Location_Name": null, "Location_Street": null, @@ -501,18 +501,18 @@ "Buffer": 0, "Hysteresis": 0, "Constructed": 156, - "Available": 52, - "DisplayValue": 52, - "Free": 52, - "Occupied": 104, + "Available": 26, + "DisplayValue": 26, + "Free": 26, + "Occupied": 130, "Reserved": 0, "Defect": 0, - "OccupiedPercent": 66.666666666666671, + "OccupiedPercent": 83.333333333333343, "Precount": 0, "Exceeded": 0, - "RealFree": 52, - "RealOccupied": 104, - "RealOccupiedPercent": 33.333333333333343, + "RealFree": 26, + "RealOccupied": 130, + "RealOccupiedPercent": 16.666666666666657, "Comment": null, "Location_Name": null, "Location_Street": null, @@ -559,18 +559,18 @@ "Buffer": 0, "Hysteresis": 0, "Constructed": 61, - "Available": 46, - "DisplayValue": 46, - "Free": 46, - "Occupied": 15, + "Available": 41, + "DisplayValue": 41, + "Free": 41, + "Occupied": 20, "Reserved": 0, "Defect": 0, - "OccupiedPercent": 24.590163934426229, + "OccupiedPercent": 32.786885245901644, "Precount": 0, "Exceeded": 0, - "RealFree": 46, - "RealOccupied": 15, - "RealOccupiedPercent": 75.409836065573771, + "RealFree": 41, + "RealOccupied": 20, + "RealOccupiedPercent": 67.21311475409837, "Comment": null, "Location_Name": null, "Location_Street": null, @@ -588,18 +588,18 @@ "Buffer": 0, "Hysteresis": 0, "Constructed": 32, - "Available": 16, - "DisplayValue": 16, - "Free": 16, - "Occupied": 16, + "Available": 15, + "DisplayValue": 15, + "Free": 15, + "Occupied": 17, "Reserved": 0, "Defect": 0, - "OccupiedPercent": 50.0, + "OccupiedPercent": 53.125, "Precount": 0, "Exceeded": 0, - "RealFree": 16, - "RealOccupied": 16, - "RealOccupiedPercent": 50.0, + "RealFree": 15, + "RealOccupied": 17, + "RealOccupiedPercent": 46.875, "Comment": null, "Location_Name": null, "Location_Street": null, diff --git a/tests/converters/data/vrn_p_r_sonah.json b/tests/converters/data/vrn_p_r_sonah.json index e93c89c..34a6910 100644 --- a/tests/converters/data/vrn_p_r_sonah.json +++ b/tests/converters/data/vrn_p_r_sonah.json @@ -3,8 +3,8 @@ "LocationID": 664, "Name": "Am Güterbahnhof", "Type": "STATIC", - "FreeParking": 40.0, - "OccupiedParking": 0.0, + "FreeParking": 39.0, + "OccupiedParking": 1.0, "TotalParking": 40.0, "ParentLocations": [ "api/v3/rest/json/locations?filter=677&exclude=SubLocations&select=[0]" @@ -80,8 +80,8 @@ "LocationID": 676, "Name": "Neckargemünd", "Type": "STATIC", - "FreeParking": 17.0, - "OccupiedParking": 26.0, + "FreeParking": 15.0, + "OccupiedParking": 28.0, "TotalParking": 43.0, "ParentLocations": [], "Areas": [ @@ -148,8 +148,8 @@ "LocationID": 677, "Name": "St. Ilgen - Sandhausen", "Type": "GROUP", - "FreeParking": 101.0, - "OccupiedParking": 21.0, + "FreeParking": 108.0, + "OccupiedParking": 14.0, "TotalParking": 122.0, "ParentLocations": [], "Areas": [], @@ -174,8 +174,8 @@ "LocationID": 678, "Name": "Leimbachstraße", "Type": "STATIC", - "FreeParking": 12.0, - "OccupiedParking": 8.0, + "FreeParking": 13.0, + "OccupiedParking": 7.0, "TotalParking": 20.0, "ParentLocations": [ "api/v3/rest/json/locations?filter=677&exclude=SubLocations&select=[0]" @@ -219,8 +219,8 @@ "LocationID": 679, "Name": "Bahnhofstraße", "Type": "STATIC", - "FreeParking": 49.0, - "OccupiedParking": 13.0, + "FreeParking": 56.0, + "OccupiedParking": 6.0, "TotalParking": 62.0, "ParentLocations": [ "api/v3/rest/json/locations?filter=677&exclude=SubLocations&select=[0]"