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 48fcd97 commit faab142
Show file tree
Hide file tree
Showing 10 changed files with 188 additions and 4 deletions.
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"}]

0 comments on commit faab142

Please sign in to comment.