Skip to content

Commit

Permalink
Merge pull request #168 from JGaukrogers/161-Use-static-data-as-addit…
Browse files Browse the repository at this point in the history
…ional-data-input-for-kienzler

161 use static data as additional data input for kienzler
  • Loading branch information
the-infinity authored Nov 25, 2024
2 parents 70565fb + fe351a5 commit 21ce5de
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 10 deletions.
49 changes: 42 additions & 7 deletions src/parkapi_sources/converters/kienzler/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,18 @@
from validataclass.exceptions import ValidationError
from validataclass.validators import AnythingValidator, DataclassValidator, ListValidator

from parkapi_sources.converters.base_converter.pull import PullConverter
from parkapi_sources.converters.base_converter.pull import PullConverter, StaticGeojsonDataMixin
from parkapi_sources.exceptions import ImportParkingSiteException
from parkapi_sources.models import RealtimeParkingSiteInput, SourceInfo, StaticParkingSiteInput

from .models import KienzlerInput
from .models import KienzlerGeojsonFeatureInput, KienzlerInput


class KienzlerBasePullConverter(PullConverter):
class KienzlerBasePullConverter(PullConverter, StaticGeojsonDataMixin):
kienzler_list_validator = ListValidator(AnythingValidator(allowed_types=[dict]))
kienzler_item_validator = DataclassValidator(KienzlerInput)
use_geojson = False
geojson_feature_validator = DataclassValidator(KienzlerGeojsonFeatureInput)

@property
@abstractmethod
Expand All @@ -34,11 +36,30 @@ def __init__(self, *args, **kwargs):
]

def get_static_parking_sites(self) -> tuple[list[StaticParkingSiteInput], list[ImportParkingSiteException]]:
static_parking_site_inputs: list[StaticParkingSiteInput] = []

kienzler_parking_sites, static_parking_site_errors = self._get_kienzler_parking_sites()
for kienzler_parking_site in kienzler_parking_sites:
static_parking_site_inputs.append(kienzler_parking_site.to_static_parking_site(self.source_info.public_url))
static_parking_site_inputs = [
kienzler_parking_site.to_static_parking_site(self.source_info.public_url)
for kienzler_parking_site in kienzler_parking_sites
]

if self.use_geojson:
geojson_parking_sites, geojson_parking_site_errors = self._get_static_geojson_parking_sites()

static_geojson_parking_site_inputs_by_uid: dict[str, dict] = {}
for geojson_parking_site in geojson_parking_sites:
static_geojson_parking_site_inputs_by_uid[geojson_parking_site['uid']] = geojson_parking_site

static_parking_site_errors += geojson_parking_site_errors

# For each Kienzler entry, extend it with GeoJSON feature if the feature exists
for kienzler_parking_site in static_parking_site_inputs:
# If the uid is not known in our static data: ignore the GeoJSON data
parking_site_uid = str(kienzler_parking_site.uid)
if parking_site_uid not in static_geojson_parking_site_inputs_by_uid:
continue

# Extend static data with GeoJSON data
kienzler_parking_site.from_dict(static_geojson_parking_site_inputs_by_uid[parking_site_uid])

return static_parking_site_inputs, static_parking_site_errors

Expand Down Expand Up @@ -88,6 +109,16 @@ def _request(self) -> list[dict]:

return result_dicts

def _get_static_geojson_parking_sites(
self,
) -> tuple[list[dict], list[ImportParkingSiteException]]:
static_parking_site_inputs, import_parking_site_exceptions = (
self._get_static_parking_site_inputs_and_exceptions(
source_uid=self.source_info.uid,
)
)
return static_parking_site_inputs, import_parking_site_exceptions


class KienzlerBikeAndRidePullConverter(KienzlerBasePullConverter):
config_prefix = 'BIKE_AND_RIDE'
Expand Down Expand Up @@ -127,6 +158,7 @@ class KienzlerNeckarsulmPullConverter(KienzlerBasePullConverter):

class KienzlerOffenburgPullConverter(KienzlerBasePullConverter):
config_prefix = 'OFFENBURG'
use_geojson = True

source_info = SourceInfo(
uid='kienzler_offenburg',
Expand All @@ -139,6 +171,7 @@ class KienzlerOffenburgPullConverter(KienzlerBasePullConverter):

class KienzlerRadSafePullConverter(KienzlerBasePullConverter):
config_prefix = 'RADSAFE'
use_geojson = True

source_info = SourceInfo(
uid='kienzler_rad_safe',
Expand All @@ -151,6 +184,7 @@ class KienzlerRadSafePullConverter(KienzlerBasePullConverter):

class KienzlerStuttgartPullConverter(KienzlerBasePullConverter):
config_prefix = 'STUTTGART'
use_geojson = True

source_info = SourceInfo(
uid='kienzler_stuttgart',
Expand All @@ -163,6 +197,7 @@ class KienzlerStuttgartPullConverter(KienzlerBasePullConverter):

class KienzlerVrnPullConverter(KienzlerBasePullConverter):
config_prefix = 'VRN'
use_geojson = True

source_info = SourceInfo(
uid='kienzler_vrn',
Expand Down
48 changes: 45 additions & 3 deletions src/parkapi_sources/converters/kienzler/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,21 @@

from datetime import datetime, timezone
from decimal import Decimal
from typing import Optional

from validataclass.dataclasses import validataclass
from validataclass.validators import IntegerValidator, NumericValidator, StringValidator
from validataclass.dataclasses import Default, ValidataclassMixin, validataclass
from validataclass.validators import (
DataclassValidator,
EnumValidator,
IntegerValidator,
ListValidator,
NumericValidator,
StringValidator,
)

from parkapi_sources.converters.base_converter.pull.static_geojson_data_mixin.models import GeojsonFeatureInput
from parkapi_sources.models import RealtimeParkingSiteInput, StaticParkingSiteInput
from parkapi_sources.models.enums import ParkingSiteType, PurposeType
from parkapi_sources.models.enums import ExternalIdentifierType, ParkAndRideType, ParkingSiteType, PurposeType


@validataclass
Expand Down Expand Up @@ -45,3 +54,36 @@ def to_realtime_parking_site(self) -> RealtimeParkingSiteInput:
realtime_capacity=self.sum_boxes,
realtime_free_capacity=self.bookable,
)


@validataclass
class ExternalIdentifier(ValidataclassMixin):
type: ExternalIdentifierType = EnumValidator(ExternalIdentifierType)
value: str = StringValidator()


@validataclass
class KienzlerGeojsonFeaturePropertiesInput(ValidataclassMixin):
uid: str = StringValidator(min_length=1, max_length=256)
address: Optional[str] = StringValidator(max_length=512), Default(None)
type: Optional[ParkingSiteType] = EnumValidator(ParkingSiteType), Default(None)
max_height: int = IntegerValidator(min_value=0)
max_width: int = IntegerValidator(min_value=0)
max_depth: int = IntegerValidator(min_value=0)
park_and_ride_type: list[ParkAndRideType] = ListValidator(EnumValidator(ParkAndRideType))
external_identifiers: Optional[list[ExternalIdentifier]] = ListValidator(DataclassValidator(ExternalIdentifier))


@validataclass
class KienzlerGeojsonFeatureInput(GeojsonFeatureInput):
properties: KienzlerGeojsonFeaturePropertiesInput = DataclassValidator(KienzlerGeojsonFeaturePropertiesInput)

def to_static_parking_site_input(self, static_data_updated_at: datetime) -> dict:
properties_dict = self.properties.to_dict()
# TODO: this property should eventually be added to the StaticParkingSiteInput class.
properties_dict.pop('max_depth', None)
return dict(
lat=self.geometry.coordinates[1],
lon=self.geometry.coordinates[0],
**properties_dict,
)
5 changes: 5 additions & 0 deletions src/parkapi_sources/models/parking_site_inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ def __post_init__(self):
),
)

def from_dict(self, data: dict):
for field in data.keys():
setattr(self, field, data[field])
return self


@validataclass
class RealtimeParkingSiteInput(BaseParkingSiteInput):
Expand Down
31 changes: 31 additions & 0 deletions tests/converters/data/kienzler.geojson
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"uid": "unit1676",
"type": "TWO_TIER",
"max_height": 1250,
"max_width": 800,
"max_depth": 1970,
"park_and_ride_type": [
"TRAIN"
],
"external_identifiers": [
{
"type": "DHID",
"value": "de:08317:14500_Parent"
}
]
},
"geometry": {
"type": "Point",
"coordinates": [
7.947474,
48.475546
]
}
}
]
}
58 changes: 58 additions & 0 deletions tests/converters/kienzler_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt.
"""

from decimal import Decimal
from pathlib import Path
from unittest.mock import Mock

import pytest
from requests_mock import Mocker
from validataclass.validators import DataclassValidator

from parkapi_sources.converters import KienzlerBikeAndRidePullConverter
from parkapi_sources.converters.kienzler.models import KienzlerGeojsonFeatureInput
from parkapi_sources.models.enums import ParkAndRideType
from tests.converters.helper import validate_realtime_parking_site_inputs, validate_static_parking_site_inputs


Expand All @@ -24,12 +28,27 @@ def requests_mock_kienzler(requests_mock: Mocker) -> Mocker:
return requests_mock


@pytest.fixture
def requests_mock_kienzler_with_geojson(requests_mock: Mocker) -> Mocker:
json_path = Path(Path(__file__).parent, 'data', 'kienzler.geojson')
with json_path.open() as json_file:
json_data = json_file.read()

requests_mock.get(
'https://raw.githubusercontent.com/ParkenDD/parkapi-static-data/refs/heads/main/sources/kienzler_bike_and_ride.geojson',
text=json_data,
)

return requests_mock


@pytest.fixture
def kienzler_config_helper(mocked_config_helper: Mock):
config = {
'PARK_API_KIENZLER_BIKE_AND_RIDE_USER': '01275925-742c-460b-8778-eca90eb114bc',
'PARK_API_KIENZLER_BIKE_AND_RIDE_PASSWORD': '626027f2-66e9-40bd-8ff2-4c010f5eca05',
'PARK_API_KIENZLER_BIKE_AND_RIDE_IDS': 'id1,id2,id3',
'STATIC_GEOJSON_BASE_URL': 'https://raw.githubusercontent.com/ParkenDD/parkapi-static-data/refs/heads/main/sources',
}
mocked_config_helper.get.side_effect = lambda key, default=None: config.get(key, default)
return mocked_config_helper
Expand All @@ -40,6 +59,14 @@ def kienzler_pull_converter(kienzler_config_helper: Mock) -> KienzlerBikeAndRide
return KienzlerBikeAndRidePullConverter(config_helper=kienzler_config_helper)


@pytest.fixture
def kienzler_pull_converter_with_geojson(kienzler_config_helper: Mock) -> KienzlerBikeAndRidePullConverter:
converter = KienzlerBikeAndRidePullConverter(config_helper=kienzler_config_helper)
converter.use_geojson = True
converter.geojson_feature_validator = DataclassValidator(KienzlerGeojsonFeatureInput)
return converter


class KienzlerPullConverterTest:
@staticmethod
def test_get_static_parking_sites(
Expand All @@ -53,6 +80,37 @@ def test_get_static_parking_sites(

validate_static_parking_site_inputs(static_parking_site_inputs)

@staticmethod
def test_get_static_parking_sites_with_geojson(
kienzler_pull_converter_with_geojson: KienzlerBikeAndRidePullConverter,
requests_mock_kienzler: Mocker,
requests_mock_kienzler_with_geojson: Mocker,
):
static_parking_site_inputs, import_parking_site_exceptions = (
kienzler_pull_converter_with_geojson.get_static_parking_sites()
)

assert len(static_parking_site_inputs) == 5
assert len(import_parking_site_exceptions) == 0

# Check that the data has been updated
static_parking_site_input = [
realtime_parking_site_input
for realtime_parking_site_input in static_parking_site_inputs
if realtime_parking_site_input.uid == 'unit1676'
][0]
assert static_parking_site_input.uid == 'unit1676'
assert static_parking_site_input.type.value == 'TWO_TIER'
assert static_parking_site_input.max_height == 1250
assert static_parking_site_input.max_width == 800
assert static_parking_site_input.park_and_ride_type == [ParkAndRideType.TRAIN]
assert static_parking_site_input.external_identifiers[0]['type'].value == 'DHID'
assert static_parking_site_input.external_identifiers[0]['value'] == 'de:08317:14500_Parent'
assert static_parking_site_input.lat == Decimal('48.475546')
assert static_parking_site_input.lon == Decimal('7.947474')

validate_static_parking_site_inputs(static_parking_site_inputs)

@staticmethod
def test_get_realtime_parking_sites(
kienzler_pull_converter: KienzlerBikeAndRidePullConverter,
Expand Down

0 comments on commit 21ce5de

Please sign in to comment.