Skip to content

support basel #86

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
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
4 changes: 2 additions & 2 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
67 changes: 67 additions & 0 deletions src/parkapi_sources/converters/basel/converter.py
Original file line number Diff line number Diff line change
@@ -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
60 changes: 60 additions & 0 deletions src/parkapi_sources/converters/basel/models.py
Original file line number Diff line number Diff line change
@@ -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,
)
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
44 changes: 44 additions & 0 deletions tests/converters/basel_test.py
Original file line number Diff line number Diff line change
@@ -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)
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"}]