Implement API endpoints
This commit is contained in:
parent
20bdce68a3
commit
9847ba105c
21
projects/project-3/backend/api.py
Normal file
21
projects/project-3/backend/api.py
Normal 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)
|
101
projects/project-3/backend/api_database.py
Normal file
101
projects/project-3/backend/api_database.py
Normal 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()
|
0
projects/project-3/backend/routers/__init__.py
Normal file
0
projects/project-3/backend/routers/__init__.py
Normal file
36
projects/project-3/backend/routers/accidents.py
Normal file
36
projects/project-3/backend/routers/accidents.py
Normal 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)
|
61
projects/project-3/backend/routers/bikepoints.py
Normal file
61
projects/project-3/backend/routers/bikepoints.py
Normal 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)
|
71
projects/project-3/backend/routers/dashboard.py
Normal file
71
projects/project-3/backend/routers/dashboard.py
Normal 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)
|
Loading…
Reference in New Issue
Block a user