Skip to content

Commit faab142

Browse files
committed
support basel
1 parent 48fcd97 commit faab142

File tree

10 files changed

+188
-4
lines changed

10 files changed

+188
-4
lines changed

CHANGELOG.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
# Changelog
22

3-
[Unreleased]
3+
## 0.7.1
4+
5+
Released 2024-07-25
46

57
### Fixes
68

79
* [Fix Ulm scraper](https://github.com/ParkenDD/parkapi-sources-v3/pull/82)
810
* [Fix Herrenberg address](https://github.com/ParkenDD/parkapi-sources-v3/pull/83)
911
* [Fix Herrenberg state mapping](https://github.com/ParkenDD/parkapi-sources-v3/pull/84)
12+
* [Fix BFRK is_covered naming](https://github.com/ParkenDD/parkapi-sources-v3/pull/85)
1013

1114

1215
## 0.7.0

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 |

requirements-dev.txt

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
ruff~=0.5.1
2-
pytest~=8.2.2
1+
ruff~=0.5.4
2+
pytest~=8.3.1
33
pytest-cov~=5.0.0
44
requests-mock~=1.12.1
55
tox~=4.16.0

requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ lxml~=5.2.2
44
openpyxl~=3.1.5
55
requests~=2.32.3
66
beautifulsoup4~=4.12.3
7-
urllib3~=2.2.1
7+
urllib3~=2.2.2
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,67 @@
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+
import requests
7+
from validataclass.exceptions import ValidationError
8+
from validataclass.validators import AnythingValidator, DataclassValidator, ListValidator
9+
10+
from parkapi_sources.converters.base_converter.pull import PullConverter
11+
from parkapi_sources.converters.basel.models import BaselParkingSiteInput
12+
from parkapi_sources.exceptions import ImportParkingSiteException
13+
from parkapi_sources.models import RealtimeParkingSiteInput, SourceInfo, StaticParkingSiteInput
14+
15+
16+
class BaselPullConverter(PullConverter):
17+
parking_sites_input_validator = ListValidator(AnythingValidator(allowed_types=[dict]))
18+
parking_site_validator = DataclassValidator(BaselParkingSiteInput)
19+
20+
source_info = SourceInfo(
21+
uid='basel',
22+
name='Stadt Basel',
23+
public_url='https://www.parkleitsystem-basel.ch',
24+
source_url='https://data.bs.ch/api/v2/catalog/datasets/100088/exports/json',
25+
timezone='Europe/Berlin',
26+
attribution_contributor='Stadt Basel',
27+
has_realtime_data=True,
28+
)
29+
30+
def get_static_parking_sites(self) -> tuple[list[StaticParkingSiteInput], list[ImportParkingSiteException]]:
31+
static_parking_site_inputs: list[StaticParkingSiteInput] = []
32+
parking_site_inputs, parking_site_errors = self._get_parking_site_inputs()
33+
34+
for parking_site_input in parking_site_inputs:
35+
static_parking_site_inputs.append(parking_site_input.to_static_parking_site())
36+
37+
return static_parking_site_inputs, parking_site_errors
38+
39+
def get_realtime_parking_sites(self) -> tuple[list[RealtimeParkingSiteInput], list[ImportParkingSiteException]]:
40+
realtime_parking_site_inputs: list[RealtimeParkingSiteInput] = []
41+
parking_site_inputs, parking_site_errors = self._get_parking_site_inputs()
42+
43+
for parking_site_input in parking_site_inputs:
44+
realtime_parking_site_inputs.append(parking_site_input.to_realtime_parking_site())
45+
46+
return realtime_parking_site_inputs, parking_site_errors
47+
48+
def _get_parking_site_inputs(self) -> tuple[list[BaselParkingSiteInput], list[ImportParkingSiteException]]:
49+
parking_site_inputs: list[BaselParkingSiteInput] = []
50+
parking_site_errors: list[ImportParkingSiteException] = []
51+
52+
response = requests.get(self.source_info.source_url, timeout=60)
53+
parking_sites_dicts: list[dict] = self.parking_sites_input_validator.validate(response.json())
54+
55+
for parking_site_dict in parking_sites_dicts:
56+
try:
57+
parking_site_inputs.append(self.parking_site_validator.validate(parking_site_dict))
58+
except ValidationError as e:
59+
parking_site_errors.append(
60+
ImportParkingSiteException(
61+
source_uid=self.source_info.uid,
62+
parking_site_uid=parking_site_dict.get('id'),
63+
message=f'validation error for static data {parking_site_dict}: {e.to_dict()}',
64+
),
65+
)
66+
67+
return parking_site_inputs, parking_site_errors
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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 (
11+
DataclassValidator,
12+
DateTimeValidator,
13+
IntegerValidator,
14+
NumericValidator,
15+
StringValidator,
16+
UrlValidator,
17+
)
18+
19+
from parkapi_sources.models import RealtimeParkingSiteInput, StaticParkingSiteInput
20+
21+
22+
@validataclass
23+
class BaselCoordinates:
24+
lat: Decimal = NumericValidator()
25+
lon: Decimal = NumericValidator()
26+
27+
28+
@validataclass
29+
class BaselParkingSiteInput:
30+
title: str = StringValidator()
31+
id2: str = StringValidator()
32+
name: str = StringValidator()
33+
total: int = IntegerValidator(min_value=0)
34+
free: int = IntegerValidator(min_value=0)
35+
link: str = UrlValidator()
36+
geo_point_2d: BaselCoordinates = DataclassValidator(BaselCoordinates)
37+
published: datetime = DateTimeValidator(
38+
local_timezone=timezone.utc,
39+
target_timezone=timezone.utc,
40+
discard_milliseconds=True,
41+
)
42+
43+
def to_static_parking_site(self) -> StaticParkingSiteInput:
44+
return StaticParkingSiteInput(
45+
uid=self.id2,
46+
name=self.name,
47+
lat=self.geo_point_2d.lat,
48+
lon=self.geo_point_2d.lon,
49+
capacity=self.total,
50+
public_url=self.link,
51+
static_data_updated_at=self.published,
52+
)
53+
54+
def to_realtime_parking_site(self) -> RealtimeParkingSiteInput:
55+
return RealtimeParkingSiteInput(
56+
uid=self.id2,
57+
realtime_capacity=self.total,
58+
realtime_free_capacity=self.free,
59+
realtime_data_updated_at=self.published,
60+
)

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

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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+
32+
assert len(static_parking_site_inputs) == 16
33+
assert len(import_parking_site_exceptions) == 1
34+
35+
validate_static_parking_site_inputs(static_parking_site_inputs)
36+
37+
@staticmethod
38+
def test_get_realtime_parking_sites(basel_pull_converter: BaselPullConverter):
39+
realtime_parking_site_inputs, import_parking_site_exceptions = basel_pull_converter.get_realtime_parking_sites()
40+
41+
assert len(realtime_parking_site_inputs) == 16
42+
assert len(import_parking_site_exceptions) == 1
43+
44+
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)