Skip to content

Commit a238cc3

Browse files
committed
support basel
1 parent c4fa620 commit a238cc3

File tree

7 files changed

+178
-0
lines changed

7 files changed

+178
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ We support following data sources:
1010
|-----------------------------------------------------------------------------------|---------|-------------|------------------------|----------|
1111
| APCOA Services | car | pull | `apcoa` | no |
1212
| Deutsche Bahn | car | pull | `bahn_v2` | no |
13+
| Stadt Basel | car | pull | `basel` | yes |
1314
| Stadt Bietigheim-Bissingen | car | pull | `bietigheim_bissingen` | yes |
1415
| Barrierefreie Reisekette Baden-Württemberg: PKW-Parkplätze an Bahnhöfen | car | push (csv) | `bfrk_bw_oepnv_car` | no |
1516
| Barrierefreie Reisekette Baden-Württemberg: PKW-Parkplätze an Bushaltestellen | car | push (csv) | `bfrk_bw_spnv_car` | no |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""
2+
Copyright 2024 binary butterfly GmbH
3+
Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt.
4+
"""
5+
6+
from .converter import BaselPullConverter
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""
2+
Copyright 2024 binary butterfly GmbH
3+
Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt.
4+
"""
5+
6+
from datetime import datetime
7+
8+
import requests
9+
from validataclass.exceptions import ValidationError
10+
from validataclass.validators import DataclassValidator, ListValidator, AnythingValidator
11+
12+
from parkapi_sources.converters.base_converter.pull import PullConverter
13+
from parkapi_sources.converters.basel.models import BaselParkingSiteInput
14+
from parkapi_sources.exceptions import ImportParkingSiteException
15+
from parkapi_sources.models import RealtimeParkingSiteInput, SourceInfo, StaticParkingSiteInput
16+
17+
18+
class BaselPullConverter(PullConverter):
19+
parking_sites_input_validator = ListValidator(AnythingValidator(allowed_types=[dict]))
20+
parking_site_validator = DataclassValidator(BaselParkingSiteInput)
21+
22+
source_info = SourceInfo(
23+
uid='basel',
24+
name='Stadt Basel',
25+
public_url='https://www.parkleitsystem-basel.ch',
26+
source_url='https://data.bs.ch/api/v2/catalog/datasets/100088/exports/json',
27+
timezone='Europe/Berlin',
28+
attribution_contributor='Stadt Basel',
29+
has_realtime_data=True,
30+
)
31+
32+
def get_static_parking_sites(self) -> tuple[list[StaticParkingSiteInput], list[ImportParkingSiteException]]:
33+
static_parking_site_inputs: list[StaticParkingSiteInput] = []
34+
parking_site_inputs, parking_site_errors = self._get_parking_site_inputs()
35+
36+
for parking_site_input in parking_site_inputs:
37+
static_parking_site_inputs.append(parking_site_input.to_static_parking_site())
38+
39+
return static_parking_site_inputs, parking_site_errors
40+
41+
def get_realtime_parking_sites(self) -> tuple[list[RealtimeParkingSiteInput], list[ImportParkingSiteException]]:
42+
realtime_parking_site_inputs: list[RealtimeParkingSiteInput] = []
43+
parking_site_inputs, parking_site_errors = self._get_parking_site_inputs()
44+
45+
for parking_site_input in parking_site_inputs:
46+
realtime_parking_site_inputs.append(parking_site_input.to_realtime_parking_site())
47+
48+
return realtime_parking_site_inputs, parking_site_errors
49+
50+
def _get_parking_site_inputs(self) -> tuple[list[BaselParkingSiteInput], list[ImportParkingSiteException]]:
51+
parking_site_inputs: list[BaselParkingSiteInput] = []
52+
parking_site_errors: list[ImportParkingSiteException] = []
53+
54+
response = requests.get(self.source_info.source_url, timeout=60)
55+
parking_sites_dicts: list[dict] = self.parking_sites_input_validator.validate(response.json())
56+
57+
for parking_site_dict in parking_sites_dicts:
58+
try:
59+
parking_site_inputs.append(self.parking_site_validator.validate(parking_site_dict))
60+
except ValidationError as e:
61+
parking_site_errors.append(
62+
ImportParkingSiteException(
63+
source_uid=self.source_info.uid,
64+
parking_site_uid=parking_site_dict.get('id'),
65+
message=f'validation error for static data {parking_site_dict}: {e.to_dict()}',
66+
),
67+
)
68+
69+
return parking_site_inputs, parking_site_errors
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""
2+
Copyright 2024 binary butterfly GmbH
3+
Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt.
4+
"""
5+
6+
from datetime import datetime, timezone
7+
from decimal import Decimal
8+
9+
from validataclass.dataclasses import validataclass
10+
from validataclass.validators import StringValidator, IntegerValidator, NumericValidator, UrlValidator, DateTimeValidator, \
11+
DataclassValidator
12+
13+
from parkapi_sources.models import StaticParkingSiteInput, RealtimeParkingSiteInput
14+
15+
16+
@validataclass
17+
class BaselCoordinates:
18+
lat: Decimal = NumericValidator()
19+
lon: Decimal = NumericValidator()
20+
21+
22+
@validataclass
23+
class BaselParkingSiteInput:
24+
title: str = StringValidator()
25+
id2: str = StringValidator()
26+
name: str = StringValidator()
27+
total: int = IntegerValidator(min_value=0)
28+
free: int = IntegerValidator(min_value=0)
29+
link: str = UrlValidator()
30+
geo_point_2d: BaselCoordinates = DataclassValidator(BaselCoordinates)
31+
published: datetime = DateTimeValidator(
32+
local_timezone=timezone.utc,
33+
target_timezone=timezone.utc,
34+
discard_milliseconds=True,
35+
)
36+
37+
def to_static_parking_site(self) -> StaticParkingSiteInput:
38+
return StaticParkingSiteInput(
39+
uid=self.id2,
40+
name=self.name,
41+
lat=self.geo_point_2d.lat,
42+
lon=self.geo_point_2d.lon,
43+
capacity=self.total,
44+
public_url=self.link,
45+
static_data_updated_at=self.published,
46+
)
47+
48+
def to_realtime_parking_site(self) -> RealtimeParkingSiteInput:
49+
return RealtimeParkingSiteInput(
50+
uid=self.id2,
51+
realtime_capacity=self.total,
52+
realtime_free_capacity=self.free,
53+
realtime_data_updated_at=self.published,
54+
)

src/parkapi_sources/parkapi_sources.py

+2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
)
4141
from .converters.base_converter.pull import PullConverter
4242
from .converters.base_converter.push import PushConverter
43+
from .converters.basel import BaselPullConverter
4344
from .exceptions import MissingConfigException, MissingConverterException
4445
from .util import ConfigHelper
4546

@@ -48,6 +49,7 @@ class ParkAPISources:
4849
converter_classes: list[Type[BaseConverter]] = [
4950
ApcoaPullConverter,
5051
BahnV2PullConverter,
52+
BaselPullConverter,
5153
BfrkBwOepnvBikePushConverter,
5254
BfrkBwOepnvCarPushConverter,
5355
BfrkBwSpnvBikePushConverter,

tests/converters/basel_test.py

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""
2+
Copyright 2024 binary butterfly GmbH
3+
Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt.
4+
"""
5+
6+
from pathlib import Path
7+
from unittest.mock import Mock
8+
9+
import pytest
10+
from parkapi_sources.converters.basel import BaselPullConverter
11+
from requests_mock import Mocker
12+
13+
from tests.converters.helper import validate_realtime_parking_site_inputs, validate_static_parking_site_inputs
14+
15+
16+
@pytest.fixture
17+
def basel_pull_converter(mocked_config_helper: Mock, requests_mock: Mocker) -> BaselPullConverter:
18+
json_path = Path(Path(__file__).parent, 'data', 'basel.json')
19+
with json_path.open() as json_file:
20+
json_data = json_file.read()
21+
22+
requests_mock.get('https://data.bs.ch/api/v2/catalog/datasets/100088/exports/json', text=json_data)
23+
24+
return BaselPullConverter(config_helper=mocked_config_helper)
25+
26+
27+
class BaselPullConverterTest:
28+
@staticmethod
29+
def test_get_static_parking_sites(basel_pull_converter: BaselPullConverter):
30+
static_parking_site_inputs, import_parking_site_exceptions = basel_pull_converter.get_static_parking_sites()
31+
print(import_parking_site_exceptions)
32+
33+
assert len(static_parking_site_inputs) == 16
34+
assert len(import_parking_site_exceptions) == 1
35+
36+
validate_static_parking_site_inputs(static_parking_site_inputs)
37+
38+
@staticmethod
39+
def test_get_realtime_parking_sites(basel_pull_converter: BaselPullConverter):
40+
realtime_parking_site_inputs, import_parking_site_exceptions = basel_pull_converter.get_realtime_parking_sites()
41+
42+
assert len(realtime_parking_site_inputs) == 16
43+
assert len(import_parking_site_exceptions) == 1
44+
45+
validate_realtime_parking_site_inputs(realtime_parking_site_inputs)

tests/converters/data/basel.json

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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 commit comments

Comments
 (0)