Skip to content

Commit

Permalink
support basel
Browse files Browse the repository at this point in the history
  • Loading branch information
the-infinity committed Jul 28, 2024
1 parent c4fa620 commit a238cc3
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ We support following data sources:
|-----------------------------------------------------------------------------------|---------|-------------|------------------------|----------|
| APCOA Services | car | pull | `apcoa` | no |
| Deutsche Bahn | car | pull | `bahn_v2` | no |
| Stadt Basel | car | pull | `basel` | yes |
| Stadt Bietigheim-Bissingen | car | pull | `bietigheim_bissingen` | yes |
| Barrierefreie Reisekette Baden-Württemberg: PKW-Parkplätze an Bahnhöfen | car | push (csv) | `bfrk_bw_oepnv_car` | no |
| Barrierefreie Reisekette Baden-Württemberg: PKW-Parkplätze an Bushaltestellen | car | push (csv) | `bfrk_bw_spnv_car` | no |
Expand Down
6 changes: 6 additions & 0 deletions src/parkapi_sources/converters/basel/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""
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 BaselPullConverter
69 changes: 69 additions & 0 deletions src/parkapi_sources/converters/basel/converter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""
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

Check failure on line 6 in src/parkapi_sources/converters/basel/converter.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (F401)

src/parkapi_sources/converters/basel/converter.py:6:22: F401 `datetime.datetime` imported but unused

import requests
from validataclass.exceptions import ValidationError
from validataclass.validators import DataclassValidator, ListValidator, AnythingValidator

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


class BaselPullConverter(PullConverter):

Check failure on line 18 in src/parkapi_sources/converters/basel/converter.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (I001)

src/parkapi_sources/converters/basel/converter.py:6:1: I001 Import block is un-sorted or un-formatted
parking_sites_input_validator = ListValidator(AnythingValidator(allowed_types=[dict]))
parking_site_validator = DataclassValidator(BaselParkingSiteInput)

source_info = SourceInfo(
uid='basel',
name='Stadt Basel',
public_url='https://www.parkleitsystem-basel.ch',
source_url='https://data.bs.ch/api/v2/catalog/datasets/100088/exports/json',
timezone='Europe/Berlin',
attribution_contributor='Stadt Basel',
has_realtime_data=True,
)

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

for parking_site_input in parking_site_inputs:
static_parking_site_inputs.append(parking_site_input.to_static_parking_site())

return static_parking_site_inputs, parking_site_errors

def get_realtime_parking_sites(self) -> tuple[list[RealtimeParkingSiteInput], list[ImportParkingSiteException]]:
realtime_parking_site_inputs: list[RealtimeParkingSiteInput] = []
parking_site_inputs, parking_site_errors = self._get_parking_site_inputs()

for parking_site_input in parking_site_inputs:
realtime_parking_site_inputs.append(parking_site_input.to_realtime_parking_site())

return realtime_parking_site_inputs, parking_site_errors

def _get_parking_site_inputs(self) -> tuple[list[BaselParkingSiteInput], list[ImportParkingSiteException]]:
parking_site_inputs: list[BaselParkingSiteInput] = []
parking_site_errors: list[ImportParkingSiteException] = []

response = requests.get(self.source_info.source_url, timeout=60)
parking_sites_dicts: list[dict] = self.parking_sites_input_validator.validate(response.json())

for parking_site_dict in parking_sites_dicts:
try:
parking_site_inputs.append(self.parking_site_validator.validate(parking_site_dict))
except ValidationError as e:
parking_site_errors.append(
ImportParkingSiteException(
source_uid=self.source_info.uid,
parking_site_uid=parking_site_dict.get('id'),
message=f'validation error for static data {parking_site_dict}: {e.to_dict()}',
),
)

return parking_site_inputs, parking_site_errors
54 changes: 54 additions & 0 deletions src/parkapi_sources/converters/basel/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""
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 decimal import Decimal

from validataclass.dataclasses import validataclass
from validataclass.validators import StringValidator, IntegerValidator, NumericValidator, UrlValidator, DateTimeValidator, \
DataclassValidator

from parkapi_sources.models import StaticParkingSiteInput, RealtimeParkingSiteInput


@validataclass

Check failure on line 16 in src/parkapi_sources/converters/basel/models.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (I001)

src/parkapi_sources/converters/basel/models.py:6:1: I001 Import block is un-sorted or un-formatted
class BaselCoordinates:
lat: Decimal = NumericValidator()
lon: Decimal = NumericValidator()


@validataclass
class BaselParkingSiteInput:
title: str = StringValidator()
id2: str = StringValidator()
name: str = StringValidator()
total: int = IntegerValidator(min_value=0)
free: int = IntegerValidator(min_value=0)
link: str = UrlValidator()
geo_point_2d: BaselCoordinates = DataclassValidator(BaselCoordinates)
published: datetime = DateTimeValidator(
local_timezone=timezone.utc,
target_timezone=timezone.utc,
discard_milliseconds=True,
)

def to_static_parking_site(self) -> StaticParkingSiteInput:
return StaticParkingSiteInput(
uid=self.id2,
name=self.name,
lat=self.geo_point_2d.lat,
lon=self.geo_point_2d.lon,
capacity=self.total,
public_url=self.link,
static_data_updated_at=self.published,
)

def to_realtime_parking_site(self) -> RealtimeParkingSiteInput:
return RealtimeParkingSiteInput(
uid=self.id2,
realtime_capacity=self.total,
realtime_free_capacity=self.free,
realtime_data_updated_at=self.published,
)
2 changes: 2 additions & 0 deletions src/parkapi_sources/parkapi_sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
)
from .converters.base_converter.pull import PullConverter
from .converters.base_converter.push import PushConverter
from .converters.basel import BaselPullConverter
from .exceptions import MissingConfigException, MissingConverterException
from .util import ConfigHelper

Expand All @@ -48,6 +49,7 @@ class ParkAPISources:
converter_classes: list[Type[BaseConverter]] = [
ApcoaPullConverter,
BahnV2PullConverter,
BaselPullConverter,
BfrkBwOepnvBikePushConverter,
BfrkBwOepnvCarPushConverter,
BfrkBwSpnvBikePushConverter,
Expand Down
45 changes: 45 additions & 0 deletions tests/converters/basel_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""
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.basel import BaselPullConverter
from requests_mock import Mocker

from tests.converters.helper import validate_realtime_parking_site_inputs, validate_static_parking_site_inputs


@pytest.fixture
def basel_pull_converter(mocked_config_helper: Mock, requests_mock: Mocker) -> BaselPullConverter:
json_path = Path(Path(__file__).parent, 'data', 'basel.json')
with json_path.open() as json_file:
json_data = json_file.read()

requests_mock.get('https://data.bs.ch/api/v2/catalog/datasets/100088/exports/json', text=json_data)

return BaselPullConverter(config_helper=mocked_config_helper)


class BaselPullConverterTest:
@staticmethod
def test_get_static_parking_sites(basel_pull_converter: BaselPullConverter):
static_parking_site_inputs, import_parking_site_exceptions = basel_pull_converter.get_static_parking_sites()
print(import_parking_site_exceptions)

Check failure on line 31 in tests/converters/basel_test.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (T201)

tests/converters/basel_test.py:31:9: T201 `print` found

assert len(static_parking_site_inputs) == 16
assert len(import_parking_site_exceptions) == 1

validate_static_parking_site_inputs(static_parking_site_inputs)

@staticmethod
def test_get_realtime_parking_sites(basel_pull_converter: BaselPullConverter):
realtime_parking_site_inputs, import_parking_site_exceptions = basel_pull_converter.get_realtime_parking_sites()

assert len(realtime_parking_site_inputs) == 16
assert len(import_parking_site_exceptions) == 1

validate_realtime_parking_site_inputs(realtime_parking_site_inputs)
1 change: 1 addition & 0 deletions tests/converters/data/basel.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"title": "Parkhaus Bad. Bahnhof", "published": "2024-07-28T12:06:00+00:00", "free": 286, "total": 750, "anteil_frei": 0.38133333333333336, "auslastung": 0.6186666666666667, "auslastung_prozent": 61.86666666666667, "link": "https://www.parkleitsystem-basel.ch/parkhaus/badbahnhof", "geo_point_2d": {"lon": 7.6089067, "lat": 47.5651794}, "description": "Anzahl freie Parkpl\u00e4tze: 286", "name": "Bad. Bahnhof", "id2": "badbahnhof"},{"title": "Parkhaus Claramatte", "published": "2024-07-28T12:06:00+00:00", "free": 113, "total": 170, "anteil_frei": 0.6647058823529411, "auslastung": 0.33529411764705885, "auslastung_prozent": 33.529411764705884, "link": "https://www.parkleitsystem-basel.ch/parkhaus/claramatte", "geo_point_2d": {"lon": 7.5946604, "lat": 47.5639644}, "description": "Anzahl freie Parkpl\u00e4tze: 113", "name": "Claramatte", "id2": "claramatte"},{"title": "Parkhaus Steinen", "published": "2024-07-28T12:06:00+00:00", "free": 244, "total": 526, "anteil_frei": 0.46387832699619774, "auslastung": 0.5361216730038023, "auslastung_prozent": 53.61216730038023, "link": "https://www.parkleitsystem-basel.ch/parkhaus/steinen", "geo_point_2d": {"lon": 7.5858936, "lat": 47.5524554}, "description": "Anzahl freie Parkpl\u00e4tze: 244", "name": "Steinen", "id2": "steinen"},{"title": "Parkhaus City", "published": "2024-07-28T12:06:00+00:00", "free": 805, "total": 1114, "anteil_frei": 0.72262118491921, "auslastung": 0.27737881508079, "auslastung_prozent": 27.737881508079, "link": "https://www.parkleitsystem-basel.ch/parkhaus/city", "geo_point_2d": {"lon": 7.5824076, "lat": 47.561101}, "description": "Anzahl freie Parkpl\u00e4tze: 805", "name": "City", "id2": "city"},{"title": "Parkhaus Storchen", "published": "2024-07-28T12:06:00+00:00", "free": 38, "total": 142, "anteil_frei": 0.2676056338028169, "auslastung": 0.7323943661971831, "auslastung_prozent": 73.23943661971832, "link": "https://www.parkleitsystem-basel.ch/parkhaus/storchen", "geo_point_2d": {"lon": 7.58658, "lat": 47.5592347}, "description": "Anzahl freie Parkpl\u00e4tze: 38", "name": "Storchen", "id2": "storchen"},{"title": "Parkhaus Aeschen", "published": "2024-07-28T12:06:00+00:00", "free": 83, "total": 97, "anteil_frei": 0.8556701030927835, "auslastung": 0.14432989690721654, "auslastung_prozent": 14.432989690721653, "link": "https://www.parkleitsystem-basel.ch/parkhaus/aeschen", "geo_point_2d": {"lon": 7.5943046, "lat": 47.5504299}, "description": "Anzahl freie Parkpl\u00e4tze: 83", "name": "Aeschen", "id2": "aeschen"},{"title": "Parkhaus Kunstmuseum", "published": "2024-07-28T12:06:00+00:00", "free": 251, "total": 350, "anteil_frei": 0.7171428571428572, "auslastung": 0.2828571428571428, "auslastung_prozent": 28.28571428571428, "link": "https://www.parkleitsystem-basel.ch/parkhaus/kunstmuseum", "geo_point_2d": {"lon": 7.5927014, "lat": 47.5545146}, "description": "Anzahl freie Parkpl\u00e4tze: 251", "name": "Kunstmuseum", "id2": "kunstmuseum"},{"title": "Zur Zeit haben wir keine aktuellen Parkhausdaten erhalten", "published": null, "free": null, "total": null, "anteil_frei": null, "auslastung": null, "auslastung_prozent": null, "link": null, "geo_point_2d": null, "description": null, "name": null, "id2": null},{"title": "Parkhaus Messe", "published": "2024-07-28T12:06:00+00:00", "free": 711, "total": 752, "anteil_frei": 0.9454787234042553, "auslastung": 0.05452127659574468, "auslastung_prozent": 5.452127659574469, "link": "https://www.parkleitsystem-basel.ch/parkhaus/messe", "geo_point_2d": {"lon": 7.602175, "lat": 47.563241}, "description": "Anzahl freie Parkpl\u00e4tze: 711", "name": "Messe", "id2": "messe"},{"title": "Parkhaus Europe", "published": "2024-07-28T12:06:00+00:00", "free": 87, "total": 120, "anteil_frei": 0.725, "auslastung": 0.275, "auslastung_prozent": 27.500000000000004, "link": "https://www.parkleitsystem-basel.ch/parkhaus/europe", "geo_point_2d": {"lon": 7.5967098, "lat": 47.5630411}, "description": "Anzahl freie Parkpl\u00e4tze: 87", "name": "Europe", "id2": "europe"},{"title": "Parkhaus Rebgasse", "published": "2024-07-28T12:06:00+00:00", "free": 242, "total": 250, "anteil_frei": 0.968, "auslastung": 0.03200000000000003, "auslastung_prozent": 3.200000000000003, "link": "https://www.parkleitsystem-basel.ch/parkhaus/rebgasse", "geo_point_2d": {"lon": 7.594263, "lat": 47.5607142}, "description": "Anzahl freie Parkpl\u00e4tze: 242", "name": "Rebgasse", "id2": "rebgasse"},{"title": "Parkhaus Clarahuus", "published": "2024-07-28T12:06:00+00:00", "free": 22, "total": 52, "anteil_frei": 0.4230769230769231, "auslastung": 0.5769230769230769, "auslastung_prozent": 57.692307692307686, "link": "https://www.parkleitsystem-basel.ch/parkhaus/clarahuus", "geo_point_2d": {"lon": 7.5917937, "lat": 47.5622725}, "description": "Anzahl freie Parkpl\u00e4tze: 22", "name": "Clarahuus", "id2": "clarahuus"},{"title": "Parkhaus Elisabethen", "published": "2024-07-28T12:06:00+00:00", "free": 542, "total": 840, "anteil_frei": 0.6452380952380953, "auslastung": 0.3547619047619047, "auslastung_prozent": 35.476190476190474, "link": "https://www.parkleitsystem-basel.ch/parkhaus/elisabethen", "geo_point_2d": {"lon": 7.5874932, "lat": 47.5506254}, "description": "Anzahl freie Parkpl\u00e4tze: 542", "name": "Elisabethen", "id2": "elisabethen"},{"title": "Parkhaus Post Basel", "published": "2024-07-28T12:06:00+00:00", "free": 71, "total": 72, "anteil_frei": 0.9861111111111112, "auslastung": 0.01388888888888884, "auslastung_prozent": 1.388888888888884, "link": "https://www.parkleitsystem-basel.ch/parkhaus/postbasel", "geo_point_2d": {"lon": 7.5929374, "lat": 47.5468617}, "description": "Anzahl freie Parkpl\u00e4tze: 71", "name": "Post Basel", "id2": "postbasel"},{"title": "Parkhaus Bahnhof S\u00fcd", "published": "2024-07-28T12:06:00+00:00", "free": 50, "total": 100, "anteil_frei": 0.5, "auslastung": 0.5, "auslastung_prozent": 50.0, "link": "https://www.parkleitsystem-basel.ch/parkhaus/bahnhofsued", "geo_point_2d": {"lon": 7.5884556, "lat": 47.5458851}, "description": "Anzahl freie Parkpl\u00e4tze: 50", "name": "Bahnhof S\u00fcd", "id2": "bahnhofsued"},{"title": "Parkhaus Anfos", "published": "2024-07-28T12:06:00+00:00", "free": 167, "total": 162, "anteil_frei": 1.0308641975308641, "auslastung": -0.030864197530864113, "auslastung_prozent": -3.0864197530864113, "link": "https://www.parkleitsystem-basel.ch/parkhaus/anfos", "geo_point_2d": {"lon": 7.593512, "lat": 47.5515968}, "description": "Anzahl freie Parkpl\u00e4tze: 167", "name": "Anfos", "id2": "anfos"},{"title": "Parkhaus Centralbahnparking", "published": "2024-07-28T12:06:00+00:00", "free": 123, "total": 286, "anteil_frei": 0.43006993006993005, "auslastung": 0.56993006993007, "auslastung_prozent": 56.993006993007, "link": "https://www.parkleitsystem-basel.ch/parkhaus/centralbahnparking", "geo_point_2d": {"lon": 7.5922975, "lat": 47.547299}, "description": "Anzahl freie Parkpl\u00e4tze: 123", "name": "Centralbahnparking", "id2": "centralbahnparking"}]

0 comments on commit a238cc3

Please sign in to comment.