Implement API endpoints

This commit is contained in:
Marcel Schwarz 2020-12-20 01:36:36 +01:00
parent 20bdce68a3
commit 9847ba105c
6 changed files with 290 additions and 0 deletions

View File

@ -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)

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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)