From 9847ba105c745940c8a4e4d13c2610652adc309e Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Sun, 20 Dec 2020 01:36:36 +0100 Subject: [PATCH] Implement API endpoints --- projects/project-3/backend/api.py | 21 ++++ projects/project-3/backend/api_database.py | 101 ++++++++++++++++++ .../project-3/backend/routers/__init__.py | 0 .../project-3/backend/routers/accidents.py | 36 +++++++ .../project-3/backend/routers/bikepoints.py | 61 +++++++++++ .../project-3/backend/routers/dashboard.py | 71 ++++++++++++ 6 files changed, 290 insertions(+) create mode 100644 projects/project-3/backend/api.py create mode 100644 projects/project-3/backend/api_database.py create mode 100644 projects/project-3/backend/routers/__init__.py create mode 100644 projects/project-3/backend/routers/accidents.py create mode 100644 projects/project-3/backend/routers/bikepoints.py create mode 100644 projects/project-3/backend/routers/dashboard.py diff --git a/projects/project-3/backend/api.py b/projects/project-3/backend/api.py new file mode 100644 index 0000000..b03323f --- /dev/null +++ b/projects/project-3/backend/api.py @@ -0,0 +1,21 @@ +import uvicorn +from fastapi import FastAPI, APIRouter + +from routers import accidents, bikepoints, dashboard + +app = FastAPI( + title="London Bikestations Dashboard API", + docs_url="/api/docs", + redoc_url="/api/redoc" +) + +v1_router = APIRouter() +v1_router.include_router(accidents.router) +v1_router.include_router(bikepoints.router) +v1_router.include_router(dashboard.router) + +app.include_router(v1_router, prefix="/api/latest") + + +if __name__ == "__main__": + uvicorn.run("api:app", host="0.0.0.0", port=8080, reload=True) diff --git a/projects/project-3/backend/api_database.py b/projects/project-3/backend/api_database.py new file mode 100644 index 0000000..aee7ef5 --- /dev/null +++ b/projects/project-3/backend/api_database.py @@ -0,0 +1,101 @@ +import sqlite3 + +UPSTREAM_BASE_URL = "https://api.tfl.gov.uk" +DB_NAME = "bike-data.db" + + +def get_db_connection(): + conn = sqlite3.connect(DB_NAME, timeout=300) + conn.row_factory = sqlite3.Row + return conn + + +# ACCIDENTS +def get_all_accidents(): + query = """SELECT id, lat, lon, location, date, severity FROM accidents""" + return get_db_connection().execute(query).fetchall() + + +def get_accidents(year: str): + query = """ + SELECT id, lat, lon, location, date, severity + FROM accidents WHERE STRFTIME('%Y', date) = ?""" + return get_db_connection().execute(query, (year,)).fetchall() + + +# DASHBOARD +def get_dashboard(station_id): + query = """ + SELECT + b.id_num as id, + b.common_name AS commonName, + b.lat, + b.lon, + max(date(u.start_date, 'unixepoch')) AS maxEndDate, + min(date(u.start_date, 'unixepoch')) AS maxStartDate + FROM usage_stats u + JOIN bike_points b ON u.start_station_id = b.id_num + WHERE u.start_station_id = ?""" + return get_db_connection().execute(query, (station_id,)).fetchall() + + +def get_dashboard_to(station_id, start_date, end_date): + query = """ + SELECT + u.start_station_name AS startStationName, + u.end_station_name AS endStationName, + count(*) AS number, + round(avg(u.duration)) AS avgDuration + FROM usage_stats u + WHERE u.start_station_id = ? AND date(u.start_date, 'unixepoch') BETWEEN ? AND ? + GROUP BY u.end_station_name + ORDER BY number DESC + LIMIT 3""" + return get_db_connection().execute(query, (station_id, start_date, end_date)).fetchall() + + +def get_dashboard_from(station_id, start_date, end_date): + query = """ + SELECT + u.start_station_name AS startStationName, + u.end_station_name AS endStationName, + count(*) AS number, + round(avg(u.duration)) AS avgDuration + FROM usage_stats u + WHERE u.end_station_id = ? AND date(u.start_date, 'unixepoch') BETWEEN ? AND ? + GROUP BY u.start_station_name + ORDER BY number DESC + LIMIT 3""" + return get_db_connection().execute(query, (station_id, start_date, end_date)).fetchall() + + +def get_dashboard_duration(station_id, start_date, end_date): + query = """ + SELECT + count(*) AS number, + CASE WHEN duration <= 300 THEN '0-5' + WHEN duration <= 900 THEN '5-15' + WHEN duration <= 1800 THEN '15-30' + WHEN duration <= 2700 THEN '30-45' + ELSE '45+' + END AS minutesGroup + FROM usage_stats + WHERE + start_station_id = ? AND + date(start_date, 'unixepoch') BETWEEN ? AND ? + GROUP BY minutesGroup""" + return get_db_connection().execute(query, (station_id, start_date, end_date)).fetchall() + + +def get_dashboard_time(station_id, start_date, end_date): + query = """ + SELECT + substr(strftime('%H:%M', start_date, 'unixepoch'), 1, 4) || '0' as timeFrame, + count(*) AS number, + round(avg(duration)) AS avgDuration + FROM usage_stats + WHERE + start_station_id = ? + AND date(start_date, 'unixepoch') BETWEEN ? AND ? + GROUP BY substr(strftime('%H:%M', start_date, 'unixepoch'), 1, 4)""" + return get_db_connection().execute(query, (station_id, start_date, end_date)).fetchall() diff --git a/projects/project-3/backend/routers/__init__.py b/projects/project-3/backend/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/projects/project-3/backend/routers/accidents.py b/projects/project-3/backend/routers/accidents.py new file mode 100644 index 0000000..e9fe942 --- /dev/null +++ b/projects/project-3/backend/routers/accidents.py @@ -0,0 +1,36 @@ +import logging +from typing import List + +from fastapi import APIRouter +from pydantic.main import BaseModel + +import api_database + +router = APIRouter(prefix="/accidents", tags=["accidents", "local"]) +LOG = logging.getLogger() + + +class Accident(BaseModel): + lat: float + lon: float + severity: str + + +@router.get( + "/", + name="Get all accidents", + description="Get all bike accidents in London.", + response_model=List[Accident] +) +def get_accidents(): + return api_database.get_all_accidents() + + +@router.get( + "/{year}", + name="Get accidents by year", + description="Get bike accidents in London for a specific year.", + response_model=List[Accident] +) +def get_accidents(year: str): + return api_database.get_accidents(year) diff --git a/projects/project-3/backend/routers/bikepoints.py b/projects/project-3/backend/routers/bikepoints.py new file mode 100644 index 0000000..ac811b5 --- /dev/null +++ b/projects/project-3/backend/routers/bikepoints.py @@ -0,0 +1,61 @@ +import json +from typing import List + +import requests +from fastapi import APIRouter +from pydantic import BaseModel + +from api_database import UPSTREAM_BASE_URL + +router = APIRouter(prefix="/bikepoints", tags=["bikepoints"]) + + +class BikepointStatus(BaseModel): + NbBikes: int + NbEmptyDocks: int + NbDocks: int + + +class Bikepoint(BaseModel): + id: str + commonName: str + lat: float + lon: float + status: BikepointStatus + + +def bikepoint_mapper(bikepoint): + mapped_point = { + "id": bikepoint['id'].removeprefix("BikePoints_"), + "url": bikepoint['url'], + "commonName": bikepoint['commonName'], + "lat": bikepoint['lat'], + "lon": bikepoint['lon'] + } + props = list(filter( + lambda p: p['key'] in ["NbBikes", "NbEmptyDocks", "NbDocks"], + bikepoint['additionalProperties'] + )) + mapped_point['status'] = {prop['key']: int(prop['value']) for prop in props} + return mapped_point + + +@router.get( + "/", + tags=["upstream"], + response_model=List[Bikepoint] +) +def get_all(): + bikepoints = json.loads(requests.get(UPSTREAM_BASE_URL + "/BikePoint").text) + mapped_points = list(map(bikepoint_mapper, bikepoints)) + return mapped_points + + +@router.get( + "/{id}", + tags=["upstream"], + response_model=Bikepoint +) +def get_single(id: int): + bikepoint = json.loads(requests.get(UPSTREAM_BASE_URL + f"/BikePoint/BikePoints_{id}").text) + return bikepoint_mapper(bikepoint) diff --git a/projects/project-3/backend/routers/dashboard.py b/projects/project-3/backend/routers/dashboard.py new file mode 100644 index 0000000..dba1edb --- /dev/null +++ b/projects/project-3/backend/routers/dashboard.py @@ -0,0 +1,71 @@ +import datetime +from typing import Optional, List + +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel + +import api_database + +router = APIRouter(prefix="/dashboard/{station_id}", tags=["dashboard", "local"]) + + +def validate_daterange(start_date: datetime.date, end_date: datetime.date): + days_requested = (end_date - start_date).days + if days_requested < 0: + raise HTTPException(status_code=400, detail="Requested date-range is negative") + + +class StationDashboard(BaseModel): + id: Optional[int] + commonName: Optional[str] + lat: Optional[float] + lon: Optional[float] + maxEndDate: Optional[datetime.date] + maxStartDate: Optional[datetime.date] + + +@router.get("/", response_model=StationDashboard) +def get_general_dashboard(station_id: int): + return api_database.get_dashboard(station_id)[0] + + +class StationDashboardTopStationsEntry(BaseModel): + startStationName: str + endStationName: str + number: int + avgDuration: int + + +@router.get("/to", response_model=List[StationDashboardTopStationsEntry]) +def get_to_dashboard_for_station(station_id: int, start_date: datetime.date, end_date: datetime.date): + validate_daterange(start_date, end_date) + return api_database.get_dashboard_to(station_id, start_date, end_date) + + +@router.get("/from", response_model=List[StationDashboardTopStationsEntry]) +def get_from_dashboard_for_station(station_id: int, start_date: datetime.date, end_date: datetime.date): + validate_daterange(start_date, end_date) + return api_database.get_dashboard_from(station_id, start_date, end_date) + + +class StationDashboardDurationGroup(BaseModel): + number: int + minutesGroup: str + + +@router.get("/duration", response_model=List[StationDashboardDurationGroup]) +def get_duration_dashboard_for_station(station_id: int, start_date: datetime.date, end_date: datetime.date): + validate_daterange(start_date, end_date) + return api_database.get_dashboard_duration(station_id, start_date, end_date) + + +class StationDashboardTimeGroup(BaseModel): + timeFrame: str + number: int + avgDuration: int + + +@router.get("/time", response_model=List[StationDashboardTimeGroup]) +def get_time_dashboard_for_station(station_id: int, start_date: datetime.date, end_date: datetime.date): + validate_daterange(start_date, end_date) + return api_database.get_dashboard_time(station_id, start_date, end_date)