From 8a5e8944217b7f7aef7af39e359df857a98382b2 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Sun, 7 May 2023 15:26:18 +0200 Subject: [PATCH] Add dataset from Partago vehicles (#155) * Add dataset from Partago vehicles * Add extra links --- README.md | 75 ++------------------ examples/README.md | 84 ++++++++++++++++++++++ examples/bluebike.py | 4 +- examples/partago.py | 23 ++++++ odp_gent/__init__.py | 3 +- odp_gent/models.py | 79 ++++++++++++++++++++- odp_gent/odp_gent.py | 19 ++++- poetry.lock | 28 +++++++- pyproject.toml | 2 + tests/fixtures/partago.json | 138 ++++++++++++++++++++++++++++++++++++ tests/test_models.py | 26 ++++++- 11 files changed, 405 insertions(+), 76 deletions(-) create mode 100644 examples/README.md create mode 100644 examples/partago.py create mode 100644 tests/fixtures/partago.json diff --git a/README.md b/README.md index e52ddd1..51e8e9e 100644 --- a/README.md +++ b/README.md @@ -39,72 +39,10 @@ You can read the following datasets with this package: - [Parking garages occupancy][garages] (12 locations) - [Park and Ride occupancy][parkandride] (5 locations) -- [BlueBike locations][bluebike] (6 locations) - -
- Click here to get more details - -### Parking garages - -Parameters: - -- **limit** (default: 10) - How many results you want to retrieve. - -| Variable | Type | Description | -| :------- | :--- | :---------- | -| `garage_id` | string | The id of the garage | -| `name` | string | The name of the garage | -| `parking_type` | string | The type of parking | -| `url` | string | The url with more information about the garage | -| `is_open` | boolean | Whether the garage is open or not | -| `free_parking` | boolean | Whether there is free parking or not | -| `temporary_closed` | boolean | Whether the garage is temporarily closed or not | -| `free_space` | integer | The amount of free parking spaces | -| `total_capacity` | integer | The total capacity of the garage | -| `availability_pct` | float | The percentage of free parking spaces | -| `occupancy_pct` | integer | The percentage of occupied parking spaces | -| `longitude` | float | The longitude of the garage | -| `latitude` | float | The latitude of the garage | -| `updated_at` | datetime | The last time the data was updated | - -### Park and Ride - -Parameters: - -- **limit** (default: 10) - How many results you want to retrieve. -- **gentse_feesten** - Whether a park and ride location is used for the [Gentse Feesten](https://gentsefeesten.stad.gent). - -| Variable | Type | Description | -| :------- | :--- | :---------- | -| `spot_id` | string | The id of the park and ride | -| `name` | string | The name of the park and ride | -| `parking_type` | string | The type of parking | -| `url` | string | The url with more information about the park and ride | -| `is_open` | boolean | Whether the park and ride is open or not | -| `free_parking` | boolean | Whether there is free parking or not | -| `temporary_closed` | boolean | Whether the park and ride is temporarily closed or not | -| `gentse_feesten` | boolean | Whether the park and ride is used for the [Gentse Feesten](https://gentsefeesten.stad.gent) | -| `free_space` | integer | The amount of free parking spaces | -| `total_capacity` | integer | The total capacity of the park and ride | -| `availability_pct` | float | The percentage of free parking spaces | -| `occupancy_pct` | integer | The percentage of occupied parking spaces | -| `longitude` | float | The longitude of the park and ride | -| `latitude` | float | The latitude of the park and ride | -| `updated_at` | datetime | The last time the data was updated | - -### BlueBikes - -| Variable | Type | Description | -| :------- | :--- | :---------- | -| `spot_id` | string | The id of the bluebike location | -| `name` | string | Name of the bluebike location | -| `spot_type` | integer | The type of the bluebike location | -| `bikes_in_use` | integer | The amount of bikes in use | -| `bikes_available` | integer | The amount of bikes available | -| `last_update` | datetime | The last time the data was updated | -| `longitude` | float | The longitude of the bluebike location | -| `latitude` | float | The latitude of the bluebike location | -
+- [BlueBike rental locations][bluebike] (6 locations) +- [Partago vehicle locations][partago] (116 locations) + +Find here [more information](examples) about the different variables and parameters per dataset with this python package. ## Example @@ -218,9 +156,10 @@ SOFTWARE. [api]: https://data.stad.gent/explore [nipkaart]: https://www.nipkaart.nl -[garages]: https://data.stad.gent/explore/dataset/bezetting-parkeergarages-real-time/information/ -[parkandride]: https://data.stad.gent/explore/dataset/real-time-bezetting-pr-gent/information/ +[garages]: https://data.stad.gent/explore/dataset/bezetting-parkeergarages-real-time/information +[parkandride]: https://data.stad.gent/explore/dataset/real-time-bezetting-pr-gent/information [bluebike]: https://data.stad.gent/explore/?disjunctive.keyword&disjunctive.theme&sort=modified&q=bluebike +[partago]: https://data.stad.gent/explore/dataset/real-time-locaties-deelwagen-partago/information [build-shield]: https://github.com/klaasnicolaas/python-odp-gent/actions/workflows/tests.yaml/badge.svg diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..df68b34 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,84 @@ +## Parking garages + +With this you can read the occupancy of the parking garages in Ghent. + +Parameters: + +- **limit** (default: 10) - How many results you want to retrieve. + +| Variable | Type | Description | +| :------- | :--- | :---------- | +| `garage_id` | string | The id of the garage | +| `name` | string | The name of the garage | +| `parking_type` | string | The type of parking | +| `url` | string | The url with more information about the garage | +| `is_open` | boolean | Whether the garage is open or not | +| `free_parking` | boolean | Whether there is free parking or not | +| `temporary_closed` | boolean | Whether the garage is temporarily closed or not | +| `free_space` | integer | The amount of free parking spaces | +| `total_capacity` | integer | The total capacity of the garage | +| `availability_pct` | float | The percentage of free parking spaces | +| `occupancy_pct` | integer | The percentage of occupied parking spaces | +| `longitude` | float | The longitude of the garage | +| `latitude` | float | The latitude of the garage | +| `updated_at` | datetime | The last time the data was updated | + +## Park and Ride + +With this you can read the occupancy of the park and rides in Ghent. + +Parameters: + +- **limit** (default: 10) - How many results you want to retrieve. +- **gentse_feesten** - Whether a park and ride location is used for the [Gentse Feesten](https://gentsefeesten.stad.gent). + +| Variable | Type | Description | +| :------- | :--- | :---------- | +| `spot_id` | string | The id of the park and ride | +| `name` | string | The name of the park and ride | +| `parking_type` | string | The type of parking | +| `url` | string | The url with more information about the park and ride | +| `is_open` | boolean | Whether the park and ride is open or not | +| `free_parking` | boolean | Whether there is free parking or not | +| `temporary_closed` | boolean | Whether the park and ride is temporarily closed or not | +| `gentse_feesten` | boolean | Whether the park and ride is used for the [Gentse Feesten](https://gentsefeesten.stad.gent) | +| `free_space` | integer | The amount of free parking spaces | +| `total_capacity` | integer | The total capacity of the park and ride | +| `availability_pct` | float | The percentage of free parking spaces | +| `occupancy_pct` | integer | The percentage of occupied parking spaces | +| `longitude` | float | The longitude of the park and ride | +| `latitude` | float | The latitude of the park and ride | +| `updated_at` | datetime | The last time the data was updated | + +## BlueBikes + +This dataset consists of information about the rental locations of the [bluebikes](https://www.blue-bike.be). + +| Variable | Type | Description | +| :------- | :--- | :---------- | +| `spot_id` | string | The id of the bluebike location | +| `name` | string | Name of the bluebike location | +| `spot_type` | integer | The type of the bluebike location | +| `bikes_in_use` | integer | The amount of bikes in use | +| `bikes_available` | integer | The amount of bikes available | +| `last_update` | datetime | The last time the data was updated | +| `longitude` | float | The longitude of the bluebike location | +| `latitude` | float | The latitude of the bluebike location | + +## Partago + +This dataset consists of information about the [Partago](https://www.partago.be) vehicles and where they are located. + +Parameters: + +- **limit** (default: 10) - How many results you want to retrieve. + +| Variable | Type | Description | +| :------- | :--- | :---------- | +| `name` | string | Name of the Partago vehicle | +| `vehicle_type` | Vehicle (dict) | The vehicle information (brand, mode, fuel etc.) | +| `picture_url` | string | The url of a picture of the vehicle | +| `station_type` | string | The type of station (free floating or fixed) | +| `last_update` | datetime | The last time the data was updated | +| `longitude` | float | The longitude of the vehicle | +| `latitude` | float | The latitude of the vehicle | diff --git a/examples/bluebike.py b/examples/bluebike.py index 46d90c1..da38d4f 100644 --- a/examples/bluebike.py +++ b/examples/bluebike.py @@ -15,8 +15,8 @@ async def main() -> None: for index, item in enumerate(bluebiks, 1): count = index print(item) - print("________________________") - print(f"{count} bluebikes found") + print("__________________________") + print(f"{count} bluebike locations found") if __name__ == "__main__": diff --git a/examples/partago.py b/examples/partago.py new file mode 100644 index 0000000..a0aa77c --- /dev/null +++ b/examples/partago.py @@ -0,0 +1,23 @@ +# pylint: disable=W0621 +"""Asynchronous Python client providing Open Data information of Gent.""" + +import asyncio + +from odp_gent import ODPGent + + +async def main() -> None: + """Fetch Partago data using the Gent API client.""" + async with ODPGent() as client: + partago_vehicles = await client.partago_vehicles(limit=120) + + count: int + for index, item in enumerate(partago_vehicles, 1): + count = index + print(item) + print("________________________") + print(f"{count} partago cars found") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/odp_gent/__init__.py b/odp_gent/__init__.py index cb61345..7243217 100644 --- a/odp_gent/__init__.py +++ b/odp_gent/__init__.py @@ -1,7 +1,7 @@ """Asynchronous Python client providing Open Data information of Gent.""" from .exceptions import ODPGentConnectionError, ODPGentError -from .models import BlueBike, Garage, ParkAndRide +from .models import BlueBike, Garage, ParkAndRide, Partago from .odp_gent import ODPGent __all__ = [ @@ -11,4 +11,5 @@ "Garage", "ParkAndRide", "BlueBike", + "Partago", ] diff --git a/odp_gent/models.py b/odp_gent/models.py index 0619d6c..fbd5be8 100644 --- a/odp_gent/models.py +++ b/odp_gent/models.py @@ -1,10 +1,13 @@ """Models for Open Data Platform of Gent.""" from __future__ import annotations +import json from dataclasses import dataclass from datetime import datetime from typing import Any +import pytz + @dataclass class Garage: @@ -177,7 +180,81 @@ def from_dict(cls: type[BlueBike], data: dict[str, Any]) -> BlueBike: spot_type=int(attr.get("type")), bikes_in_use=attr.get("bikes_in_use"), bikes_available=attr.get("bikes_available"), - last_update=datetime.strptime(attr.get("last_seen"), "%Y-%m-%dT%H:%M:%S%z"), + last_update=datetime.strptime( + attr.get("last_seen"), + "%Y-%m-%dT%H:%M:%S%z", + ).replace(tzinfo=pytz.timezone("Europe/Brussels")), longitude=float(attr.get("longitude")), latitude=float(attr.get("latitude")), ) + + +@dataclass +class Partago: + """Object representing a Partago vehicle location.""" + + name: str + vehicle_type: Vehicle + picture_url: str | None + station_type: str + last_update: datetime + + latitude: float + longitude: float + + @classmethod + def from_dict(cls: type[Partago], data: dict[str, Any]) -> Partago: + """Return a Partago vehicle object from a dictionary. + + Args: + ---- + data: The data from the API. + + Returns: + ------- + A Partago object. + """ + attr = data["fields"] + geo = data["geometry"]["coordinates"] + return cls( + name=attr.get("displayname"), + vehicle_type=Vehicle.from_dict(json.loads(attr.get("vehicleinformation"))), + picture_url=None if attr.get("picture") == "null" else attr.get("picture"), + station_type=attr.get("stationtype"), + last_update=datetime.strptime( + data["record_timestamp"], + "%Y-%m-%dT%H:%M:%S.%fZ", + ).replace(tzinfo=pytz.utc), + longitude=geo[0], + latitude=geo[1], + ) + + +@dataclass +class Vehicle: + """Object representing a general vehicle.""" + + brand: str + mode: str + fuel_type: str + transmission_type: str + + @classmethod + def from_dict(cls: type[Vehicle], data: dict[str, Any]) -> Vehicle: + """Return a Vehicle object from a dictionary. + + Args: + ---- + data: The data from the API. + + + Returns: + ------- + A Vehicle object. + """ + return cls( + brand=data["brand"], + mode=data["model"], + fuel_type=data["fuelType"], + transmission_type=data["transmissionType"], + ) diff --git a/odp_gent/odp_gent.py b/odp_gent/odp_gent.py index c6d6831..6ae221f 100644 --- a/odp_gent/odp_gent.py +++ b/odp_gent/odp_gent.py @@ -13,7 +13,7 @@ from yarl import URL from .exceptions import ODPGentConnectionError, ODPGentError -from .models import BlueBike, Garage, ParkAndRide +from .models import BlueBike, Garage, ParkAndRide, Partago @dataclass @@ -177,6 +177,23 @@ async def bluebikes(self) -> list[BlueBike]: results.append(BlueBike.from_dict(item)) return results + async def partago_vehicles(self, limit: int = 10) -> list[Partago]: + """Get list of data from Partago vehicles. + + Returns + ------- + A list of Partago objects. + """ + results: list[Partago] = [] + vehicles = await self._request( + "search/", + params={"dataset": "real-time-locaties-deelwagen-partago", "rows": limit}, + ) + + for item in vehicles["records"]: + results.append(Partago.from_dict(item)) + return results + async def close(self) -> None: """Close open client session.""" if self.session and self._close_session: diff --git a/poetry.lock b/poetry.lock index 367012f..f9e9fcc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry and should not be changed by hand. +# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. [[package]] name = "aiohttp" @@ -974,6 +974,18 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +[[package]] +name = "pytz" +version = "2023.3" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2023.3-py2.py3-none-any.whl", hash = "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"}, + {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"}, +] + [[package]] name = "pyyaml" version = "6.0" @@ -1157,6 +1169,18 @@ files = [ {file = "tomlkit-0.11.6.tar.gz", hash = "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"}, ] +[[package]] +name = "types-pytz" +version = "2023.3.0.0" +description = "Typing stubs for pytz" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "types-pytz-2023.3.0.0.tar.gz", hash = "sha256:ecdc70d543aaf3616a7e48631543a884f74205f284cefd6649ddf44c6a820aac"}, + {file = "types_pytz-2023.3.0.0-py3-none-any.whl", hash = "sha256:4fc2a7fbbc315f0b6630e0b899fd6c743705abe1094d007b0e612d10da15e0f3"}, +] + [[package]] name = "typing-extensions" version = "4.4.0" @@ -1374,4 +1398,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "aa2f76fa699fa438a3d51c2c72c617b342a151a9c3e76c35f2a01577d3aa5d27" +content-hash = "a224c6810631aaff8ef4126a2964530fbcb60ed873e920ccbd559efb4074fdb5" diff --git a/pyproject.toml b/pyproject.toml index e3260ac..1eaacca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ packages = [ aiohttp = ">=3.0.0" python = "^3.9" yarl = ">=1.6.0" +pytz = "^2023.3" [tool.poetry.urls] "Bug Tracker" = "https://github.com/klaasnicolaas/python-odp-gent/issues" @@ -51,6 +52,7 @@ pytest-asyncio = ">=0.20.3,<0.22.0" pytest-cov = "^4.0.0" yamllint = "^1.29.0" covdefaults = "^2.3.0" +types-pytz = "^2023.3.0.0" [tool.black] target-version = ['py39'] diff --git a/tests/fixtures/partago.json b/tests/fixtures/partago.json new file mode 100644 index 0000000..329ecf6 --- /dev/null +++ b/tests/fixtures/partago.json @@ -0,0 +1,138 @@ +{ + "nhits": 116, + "parameters": { + "dataset": "real-time-locaties-deelwagen-partago", + "rows": 5, + "start": 0, + "format": "json", + "timezone": "UTC" + }, + "records": [ + { + "datasetid": "real-time-locaties-deelwagen-partago", + "recordid": "dec7cecb3b8db4a588fadfdc21162f211c4011eb", + "fields": { + "picture": "https://firebasestorage.googleapis.com/v0/b/sizzling-torch-8026.appspot.com/o/images%2Fplaces%2Fquark.png?alt=media&token=281f4992-fb33-49e3-adbf-1cd51f5a45d5", + "vehicleinformation": "{\"brand\": \"Nissan\", \"model\": \"e-NV200 2018\", \"fuelType\": \"electric\", \"transmissionType\": \"automatic\"}", + "longitude": "3.717662708201117", + "geopoints": [ + 51.047994818648135, + 3.717662708201117 + ], + "geosplitst": "51.047994818648135", + "geoposition": "{\"latitude\": 51.047994818648135, \"longitude\": 3.717662708201117}", + "stationtype": "free floating", + "displayname": "Partago Quark", + "stationinformation": "Coupure rechts 6, Gent" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 3.717662708201117, + 51.047994818648135 + ] + }, + "record_timestamp": "2023-05-07T12:00:08.601Z" + }, + { + "datasetid": "real-time-locaties-deelwagen-partago", + "recordid": "d82909391781207f93af5571fd21fcc61dc3d0af", + "fields": { + "picture": "https://firebasestorage.googleapis.com/v0/b/sizzling-torch-8026.appspot.com/o/images%2Fplaces%2Fzoe_66percent.jpg?alt=media&token=948566aa-abf0-4909-8e4d-62177d686877", + "vehicleinformation": "{\"brand\": \"Renault\", \"model\": \"ZOE 50\", \"fuelType\": \"electric\", \"transmissionType\": \"automatic\"}", + "longitude": "3.7530556155420487", + "geopoints": [ + 51.044021518441454, + 3.7530556155420487 + ], + "geosplitst": "51.044021518441454", + "geoposition": "{\"latitude\": 51.044021518441454, \"longitude\": 3.7530556155420487}", + "stationtype": "free floating", + "displayname": "Partago Theo" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 3.7530556155420487, + 51.044021518441454 + ] + }, + "record_timestamp": "2023-05-07T12:00:08.601Z" + }, + { + "datasetid": "real-time-locaties-deelwagen-partago", + "recordid": "29eb965b02f7cb762598f771b4b4b69f6deb85cc", + "fields": { + "picture": "https://firebasestorage.googleapis.com/v0/b/sizzling-torch-8026.appspot.com/o/images%2Fplaces%2Fzoe_66percent.jpg?alt=media&token=948566aa-abf0-4909-8e4d-62177d686877", + "vehicleinformation": "{\"brand\": \"Renault\", \"model\": \"Zoé\", \"fuelType\": \"electric\", \"transmissionType\": \"automatic\"}", + "longitude": "3.7441249969609345", + "geopoints": [ + 51.03416087693723, + 3.7441249969609345 + ], + "geosplitst": "51.03416087693723", + "geoposition": "{\"latitude\": 51.03416087693723, \"longitude\": 3.7441249969609345}", + "stationtype": "free floating", + "displayname": "Partago Rox" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 3.7441249969609345, + 51.03416087693723 + ] + }, + "record_timestamp": "2023-05-07T12:00:08.601Z" + }, + { + "datasetid": "real-time-locaties-deelwagen-partago", + "recordid": "21d361eac2d2e1b4c8ddd9a8f897a765eacbdda2", + "fields": { + "picture": "https://firebasestorage.googleapis.com/v0/b/sizzling-torch-8026.appspot.com/o/images%2Fplaces%2Fzoe_66percent.jpg?alt=media&token=948566aa-abf0-4909-8e4d-62177d686877", + "vehicleinformation": "{\"brand\": \"Renault\", \"model\": \"Zoé\", \"fuelType\": \"electric\", \"transmissionType\": \"automatic\"}", + "longitude": "3.7313283733281333", + "geopoints": [ + 51.0628833078908, + 3.7313283733281333 + ], + "geosplitst": "51.0628833078908", + "geoposition": "{\"latitude\": 51.0628833078908, \"longitude\": 3.7313283733281333}", + "stationtype": "free floating", + "displayname": "Partago Maxwell" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 3.7313283733281333, + 51.0628833078908 + ] + }, + "record_timestamp": "2023-05-07T12:00:08.601Z" + }, + { + "datasetid": "real-time-locaties-deelwagen-partago", + "recordid": "d4d5d1514f52753faf0143e00c8f2144ab124edd", + "fields": { + "picture": "https://firebasestorage.googleapis.com/v0/b/sizzling-torch-8026.appspot.com/o/images%2Fplaces%2Fzoe_66percent.jpg?alt=media&token=948566aa-abf0-4909-8e4d-62177d686877", + "vehicleinformation": "{\"brand\": \"Renault\", \"model\": \"Zoé\", \"fuelType\": \"electric\", \"transmissionType\": \"automatic\"}", + "longitude": "3.760778443329769", + "geopoints": [ + 51.0443421974434, + 3.760778443329769 + ], + "geosplitst": "51.0443421974434", + "geoposition": "{\"latitude\": 51.0443421974434, \"longitude\": 3.760778443329769}", + "stationtype": "free floating", + "displayname": "Partago Wez" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 3.760778443329769, + 51.0443421974434 + ] + }, + "record_timestamp": "2023-05-07T12:00:08.601Z" + } + ] +} diff --git a/tests/test_models.py b/tests/test_models.py index 3041adf..b23c63f 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -2,7 +2,7 @@ from aiohttp import ClientSession from aresponses import ResponsesMockServer -from odp_gent import BlueBike, Garage, ODPGent, ParkAndRide +from odp_gent import BlueBike, Garage, ODPGent, ParkAndRide, Partago from . import load_fixtures @@ -108,3 +108,27 @@ async def test_bluebikes(aresponses: ResponsesMockServer) -> None: assert item.bikes_available == 45 assert isinstance(item.longitude, float) assert isinstance(item.latitude, float) + + +async def test_partago(aresponses: ResponsesMockServer) -> None: + """Test partago function.""" + aresponses.add( + "data.stad.gent", + "/api/records/1.0/search/", + "GET", + aresponses.Response( + status=200, + headers={"Content-Type": "application/json"}, + text=load_fixtures("partago.json"), + ), + ) + async with ClientSession() as session: + client = ODPGent(session=session) + partago_vehicles: list[Partago] = await client.partago_vehicles() + assert partago_vehicles is not None + for item in partago_vehicles: + assert item.name is not None + assert item.picture_url is not None + assert item.station_type == "free floating" + assert isinstance(item.longitude, float) + assert isinstance(item.latitude, float)