Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

161 use static data as additional data input for kienzler #168

Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 47 additions & 14 deletions src/parkapi_sources/converters/kienzler/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,36 +9,55 @@
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
def config_prefix(self):
pass

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.required_config_keys = [
f'PARK_API_KIENZLER_{self.config_prefix}_USER',
f'PARK_API_KIENZLER_{self.config_prefix}_PASSWORD',
f'PARK_API_KIENZLER_{self.config_prefix}_IDS',
]
required_config_keys = [
the-infinity marked this conversation as resolved.
Show resolved Hide resolved
f'PARK_API_KIENZLER_{config_prefix}_USER',
f'PARK_API_KIENZLER_{config_prefix}_PASSWORD',
f'PARK_API_KIENZLER_{config_prefix}_IDS',
]

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 +107,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 +156,7 @@ class KienzlerNeckarsulmPullConverter(KienzlerBasePullConverter):

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

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

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

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

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

source_info = SourceInfo(
uid='kienzler_stuttgart',
Expand All @@ -163,6 +195,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