diff --git a/CHANGELOG.md b/CHANGELOG.md index cae2466..3768387 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,15 @@ # Changelog -[Unreleased] +## 0.7.1 + +Released 2024-07-25 ### Fixes * [Fix Ulm scraper](https://github.com/ParkenDD/parkapi-sources-v3/pull/82) * [Fix Herrenberg address](https://github.com/ParkenDD/parkapi-sources-v3/pull/83) * [Fix Herrenberg state mapping](https://github.com/ParkenDD/parkapi-sources-v3/pull/84) +* [Fix BFRK is_covered naming](https://github.com/ParkenDD/parkapi-sources-v3/pull/85) ## 0.7.0 diff --git a/README.md b/README.md index 10c649b..7e94c2f 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/requirements-dev.txt b/requirements-dev.txt index 21f7b09..850abfc 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,5 @@ -ruff~=0.5.1 -pytest~=8.2.2 +ruff~=0.5.4 +pytest~=8.3.1 pytest-cov~=5.0.0 requests-mock~=1.12.1 tox~=4.16.0 diff --git a/requirements.txt b/requirements.txt index 0506860..09a8b5e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,4 @@ lxml~=5.2.2 openpyxl~=3.1.5 requests~=2.32.3 beautifulsoup4~=4.12.3 -urllib3~=2.2.1 +urllib3~=2.2.2 diff --git a/src/parkapi_sources/converters/basel/__init__.py b/src/parkapi_sources/converters/basel/__init__.py new file mode 100644 index 0000000..84bee91 --- /dev/null +++ b/src/parkapi_sources/converters/basel/__init__.py @@ -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 diff --git a/src/parkapi_sources/converters/basel/converter.py b/src/parkapi_sources/converters/basel/converter.py new file mode 100644 index 0000000..517c1c6 --- /dev/null +++ b/src/parkapi_sources/converters/basel/converter.py @@ -0,0 +1,67 @@ +""" +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. +""" + +import requests +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.basel.models import BaselParkingSiteInput +from parkapi_sources.exceptions import ImportParkingSiteException +from parkapi_sources.models import RealtimeParkingSiteInput, SourceInfo, StaticParkingSiteInput + + +class BaselPullConverter(PullConverter): + 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 diff --git a/src/parkapi_sources/converters/basel/models.py b/src/parkapi_sources/converters/basel/models.py new file mode 100644 index 0000000..53f286a --- /dev/null +++ b/src/parkapi_sources/converters/basel/models.py @@ -0,0 +1,60 @@ +""" +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 ( + DataclassValidator, + DateTimeValidator, + IntegerValidator, + NumericValidator, + StringValidator, + UrlValidator, +) + +from parkapi_sources.models import RealtimeParkingSiteInput, StaticParkingSiteInput + + +@validataclass +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, + ) diff --git a/src/parkapi_sources/parkapi_sources.py b/src/parkapi_sources/parkapi_sources.py index 3c157b6..72df735 100644 --- a/src/parkapi_sources/parkapi_sources.py +++ b/src/parkapi_sources/parkapi_sources.py @@ -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 @@ -48,6 +49,7 @@ class ParkAPISources: converter_classes: list[Type[BaseConverter]] = [ ApcoaPullConverter, BahnV2PullConverter, + BaselPullConverter, BfrkBwOepnvBikePushConverter, BfrkBwOepnvCarPushConverter, BfrkBwSpnvBikePushConverter, diff --git a/tests/converters/basel_test.py b/tests/converters/basel_test.py new file mode 100644 index 0000000..492c7b8 --- /dev/null +++ b/tests/converters/basel_test.py @@ -0,0 +1,44 @@ +""" +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() + + 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) diff --git a/tests/converters/data/basel.json b/tests/converters/data/basel.json new file mode 100644 index 0000000..8f733cd --- /dev/null +++ b/tests/converters/data/basel.json @@ -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"}]