Skip to content
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

Add datasets from bluebikes #154

Merged
merged 1 commit into from
May 6, 2023
Merged
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
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ pip install odp-gent

You can read the following datasets with this package:

- Parking garages occupancy (12 locations)
- Park and Ride occupancy (5 locations)
- [Parking garages occupancy][garages] (12 locations)
- [Park and Ride occupancy][parkandride] (5 locations)
- [BlueBike locations][bluebike] (6 locations)

<details>
<summary>Click here to get more details</summary>
Expand Down Expand Up @@ -90,6 +91,19 @@ Parameters:
| `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 |
</details>

## Example
Expand Down Expand Up @@ -204,6 +218,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/
[bluebike]: https://data.stad.gent/explore/?disjunctive.keyword&disjunctive.theme&sort=modified&q=bluebike

<!-- MARKDOWN LINKS & IMAGES -->
[build-shield]: https://github.com/klaasnicolaas/python-odp-gent/actions/workflows/tests.yaml/badge.svg
[build-url]: https://github.com/klaasnicolaas/python-odp-gent/actions/workflows/tests.yaml
Expand Down
23 changes: 23 additions & 0 deletions examples/bluebike.py
Original file line number Diff line number Diff line change
@@ -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 bluebike data using the Gent API client."""
async with ODPGent() as client:
bluebiks = await client.bluebikes()

count: int
for index, item in enumerate(bluebiks, 1):
count = index
print(item)
print("________________________")
print(f"{count} bluebikes found")


if __name__ == "__main__":
asyncio.run(main())
3 changes: 2 additions & 1 deletion odp_gent/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Asynchronous Python client providing Open Data information of Gent."""

from .exceptions import ODPGentConnectionError, ODPGentError
from .models import Garage, ParkAndRide
from .models import BlueBike, Garage, ParkAndRide
from .odp_gent import ODPGent

__all__ = [
Expand All @@ -10,4 +10,5 @@
"ODPGentError",
"Garage",
"ParkAndRide",
"BlueBike",
]
40 changes: 40 additions & 0 deletions odp_gent/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,43 @@ def convert_bool(value: str) -> bool:
latitude=geo[1],
updated_at=datetime.strptime(attr.get("lastupdate"), "%Y-%m-%dT%H:%M:%S%z"),
)


@dataclass
class BlueBike:
"""Object representing a BlueBike rental location."""

spot_id: str
name: str
spot_type: int

bikes_in_use: int
bikes_available: int
last_update: datetime

longitude: float
latitude: float

@classmethod
def from_dict(cls: type[BlueBike], data: dict[str, Any]) -> BlueBike:
"""Return a BlueBike object from a dictionary.

Args:
----
data: The data from the API.

Returns:
-------
A BlueBike object.
"""
attr = data["fields"]
return cls(
spot_id=attr.get("id"),
name=attr.get("name"),
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"),
longitude=float(attr.get("longitude")),
latitude=float(attr.get("latitude")),
)
37 changes: 30 additions & 7 deletions odp_gent/odp_gent.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from yarl import URL

from .exceptions import ODPGentConnectionError, ODPGentError
from .models import Garage, ParkAndRide
from .models import BlueBike, Garage, ParkAndRide


@dataclass
Expand Down Expand Up @@ -79,14 +79,10 @@ async def _request(
response.raise_for_status()
except asyncio.TimeoutError as exception:
msg = "Timeout occurred while connecting to the Open Data Platform API."
raise ODPGentConnectionError(
msg,
) from exception
raise ODPGentConnectionError(msg) from exception
except (ClientError, socket.gaierror) as exception:
msg = "Error occurred while communicating with Open Data Platform API."
raise ODPGentConnectionError(
msg,
) from exception
raise ODPGentConnectionError(msg) from exception

content_type = response.headers.get("Content-Type", "")
if "application/json" not in content_type:
Expand Down Expand Up @@ -154,6 +150,33 @@ async def park_and_rides(
results.append(ParkAndRide.from_dict(item))
return results

async def bluebikes(self) -> list[BlueBike]:
"""Get list of data from BlueBike locations.

Returns
-------
A list of BlueBike objects.
"""
results: list[BlueBike] = []

# Data is spread over multiple datasets
datasets: list[str] = [
"blue-bike-deelfietsen-gent-sint-pieters-st-denijslaan",
"blue-bike-deelfietsen-merelbeke-drongen-wondelgem",
"blue-bike-deelfietsen-gent-sint-pieters-m-hendrikaplein",
"blue-bike-deelfietsen-gent-dampoort",
]

for dataset in datasets:
locations = await self._request(
"search/",
params={"dataset": dataset},
)

for item in locations["records"]:
results.append(BlueBike.from_dict(item))
return results

async def close(self) -> None:
"""Close open client session."""
if self.session and self._close_session:
Expand Down
38 changes: 38 additions & 0 deletions tests/fixtures/bluebikes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"nhits": 1,
"parameters": {
"dataset": "blue-bike-deelfietsen-gent-dampoort",
"rows": 10,
"start": 0,
"format": "json",
"timezone": "UTC"
},
"records": [
{
"datasetid": "blue-bike-deelfietsen-gent-dampoort",
"recordid": "e203ac31a7941dd78ea843e78e7f43737a01474d",
"fields": {
"last_seen": "2023-05-06T20:34:59+00:00",
"longitude": "3.740194000000",
"name": "Station Gent-Dampoort",
"geopoint": [
51.056083,
3.740194
],
"latitude": "51.056083000000",
"bikes_available": 45,
"bikes_in_use": 15,
"type": "1",
"id": 72
},
"geometry": {
"type": "Point",
"coordinates": [
3.740194,
51.056083
]
},
"record_timestamp": "2023-05-06T18:40:05.35Z"
}
]
}
36 changes: 33 additions & 3 deletions tests/test_models.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
"""Test the models."""
import pytest
from aiohttp import ClientSession
from aresponses import ResponsesMockServer

from odp_gent import Garage, ODPGent, ParkAndRide
from odp_gent import BlueBike, Garage, ODPGent, ParkAndRide

from . import load_fixtures


@pytest.mark.asyncio
async def test_all_garages(aresponses: ResponsesMockServer) -> None:
"""Test all garages function."""
aresponses.add(
Expand Down Expand Up @@ -78,3 +76,35 @@ async def test_filter_park_and_rides(aresponses: ResponsesMockServer) -> None:
assert item.availability_pct is not None
assert isinstance(item.longitude, float)
assert isinstance(item.latitude, float)


async def test_bluebikes(aresponses: ResponsesMockServer) -> None:
"""Test bluebikes function."""
datasets: list[str] = [
"bluebikes.json",
"bluebikes.json",
"bluebikes.json",
"bluebikes.json",
]
for dataset in datasets:
aresponses.add(
"data.stad.gent",
"/api/records/1.0/search/",
"GET",
aresponses.Response(
status=200,
headers={"Content-Type": "application/json"},
text=load_fixtures(dataset),
),
)
async with ClientSession() as session:
client = ODPGent(session=session)
bluebikes: list[BlueBike] = await client.bluebikes()
assert bluebikes is not None
for item in bluebikes:
assert item.spot_id == 72
assert item.name == "Station Gent-Dampoort"
assert item.bikes_in_use == 15
assert item.bikes_available == 45
assert isinstance(item.longitude, float)
assert isinstance(item.latitude, float)