Skip to content

Commit

Permalink
added /standings endpoint
Browse files Browse the repository at this point in the history
* updated supported year condition for year

* updated dependencies

* added /standings endpoint with tests

* updated dependencies

* fix and refactor logic for /standings

* fix, refactor and add tests for /standings
  • Loading branch information
borolepratik authored Jan 21, 2024
1 parent 52ee7ab commit 820ecb1
Show file tree
Hide file tree
Showing 11 changed files with 4,559 additions and 1,848 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ jobs:
run: poetry install --no-root

- name: 📊 Run tests
run: poetry run pytest
run: poetry run pytest -rpP
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Table of Contents:
- [Interactive Jupyter notebook](#interactive-jupyter-notebook)
- [Using Jupyter Lab](#using-jupyter-lab)
- [Running the notebook in VS Code](#running-the-notebook-in-vs-code)
- [Running tests](#running-tests)
- [Contribution Guidelines](#contribution-guidelines)
- [Deployment](#deployment)

Expand Down Expand Up @@ -84,6 +85,12 @@ poetry install --no-root
- Alternatively, you can install the [Jupyer extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter) and run the notebook in VS Code.
- Ensure to use the poetry python environment as the kernel.
### Running tests
```sh
poetry run pytest -rpP
```
### Contribution Guidelines
- <u> _**NEVER MERGE YOUR OWN CODE; ALWAYS RAISE A PR AGAINST `dev`!**_ </u>
Expand Down
8 changes: 8 additions & 0 deletions app/constants.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from datetime import datetime

METADATA_DESCRIPTION = """
Slick Telemetry backend written in python with fastf1. 🏎
Expand All @@ -19,3 +21,9 @@
"Session5Date",
"Session5DateUtc",
]

MIN_SUPPORTED_YEAR = 1950
MAX_SUPPORTED_YEAR = datetime.today().year

MIN_SUPPORTED_ROUND = 1
MAX_SUPPORTED_ROUND = 30
128 changes: 109 additions & 19 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
import json
from datetime import datetime
from typing import Annotated
from typing import Annotated, Literal

import fastf1
from fastapi import FastAPI, Query, status
from fastapi import FastAPI, HTTPException, Query, status
from fastapi.middleware.cors import CORSMiddleware


from app.constants import EVENT_SCHEDULE_DATETIME_DTYPE_LIST, METADATA_DESCRIPTION
from app.models import HealthCheck, Schedule
from app.utils import get_default_year_for_schedule
from fastf1.ergast import Ergast

from app.constants import (
EVENT_SCHEDULE_DATETIME_DTYPE_LIST,
MAX_SUPPORTED_ROUND,
MAX_SUPPORTED_YEAR,
METADATA_DESCRIPTION,
MIN_SUPPORTED_ROUND,
MIN_SUPPORTED_YEAR,
)
from app.models import HealthCheck, Schedule, Standings
from app.utils import get_default_year

# fastf1.set_log_level("WARNING") # TODO use for production and staging


# Cors Middleware
origins = ["http://localhost:3000"]
# Ergast configuration
ergast = Ergast(result_type="raw", auto_cast=True)


app = FastAPI(
Expand All @@ -31,16 +40,14 @@
},
)

# Cors Middleware
origins = [
"http://localhost:3000"
]

app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
# HTTPSRedirectMiddleware # TODO use for production and staging
)


Expand Down Expand Up @@ -71,7 +78,8 @@ def get_health() -> HealthCheck:
to ensure a robust container orchestration and management is in place. Other
services which rely on proper functioning of the API service will not deploy if this
endpoint returns any other HTTP status code except 200 (OK).
Returns:
**Returns**:
HealthCheck: Returns a JSON response with the health status
"""
return HealthCheck(status="OK")
Expand All @@ -90,23 +98,24 @@ def get_schedule(
int | None,
Query(
title="The year for which to get the schedule",
gt=1949, # Supported years are 1950 to current
le=datetime.today().year,
ge=MIN_SUPPORTED_YEAR,
le=MAX_SUPPORTED_YEAR,
),
] = None
) -> list[Schedule]:
"""
## Get events schedule for a Formula 1 calendar year
Endpoint to get events schedule for Formula 1 calendar year.
Returns:
**Returns**:
list[Schedule]: Returns a JSON response with the list of event schedule
"""
if year is None:
year = get_default_year_for_schedule()
year = get_default_year()

event_schedule = fastf1.get_event_schedule(year)

# Convert timestamp(z) related columns' data into a string type
# Convert timestamp(z) related columns' data into string type
# https://stackoverflow.com/questions/50404559/python-error-typeerror-object-of-type-timestamp-is-not-json-serializable
for col in EVENT_SCHEDULE_DATETIME_DTYPE_LIST:
event_schedule[col] = event_schedule[col].astype(str)
Expand All @@ -118,3 +127,84 @@ def get_schedule(
event_schedule_as_json_obj = json.loads(event_schedule_as_json)

return event_schedule_as_json_obj


@app.get(
"/standings",
tags=["standings"],
summary="Get drivers and contructors standings ",
response_description="Return a list of drivers and contructors standings at specific points of a season. If the season hasn't ended you will get the current standings.",
status_code=status.HTTP_200_OK,
response_model=Standings,
)
def get_standings(
year: Annotated[
int | None,
Query(
title="The year for which to get the driver and contructors standing. If the season hasn't ended you will get the current standings.",
ge=MIN_SUPPORTED_YEAR,
le=MAX_SUPPORTED_YEAR,
),
] = None,
round: Annotated[
int | None,
Query(
title="The round in a year for which to get the driver and contructor standings",
ge=MIN_SUPPORTED_ROUND,
le=MAX_SUPPORTED_ROUND,
),
] = None,
) -> Standings:
"""
## Get driver and contructor standings
Endpoint to get driver and contructor standings at specific points of a season. If the season hasn't ended you will get the current standings.
**Returns**:
Standings: Returns a JSON response with the driver and constructor standings
"""

if year is None and round is None:
# neither year nor round are provided; get results for the last round of the default year
year = get_default_year()
elif year is None and round is not None:
# only round is provided; error out
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail='Bad request. Must provide the "year" parameter.',
)

# inputs are good; either one of the two remaining cases:
# 1. both year and round are provided
# 2. only year is provided

driver_standings = ergast.get_driver_standings(season=year, round=round)
constructor_standings = ergast.get_constructor_standings(season=year, round=round)

driver_standings_available = True if len(driver_standings) > 0 else False
constructor_standings_available = True if len(constructor_standings) > 0 else False

if driver_standings_available and constructor_standings_available:
# both driver and constructor standings are available
data: Standings = {
"season": driver_standings[0]["season"],
"round": driver_standings[0]["round"],
"DriverStandings": driver_standings[0]["DriverStandings"],
"ConstructorStandings": constructor_standings[0]["ConstructorStandings"],
}
return data
elif not driver_standings_available and not constructor_standings_available:
# neither driver nor constructor standings are available
raise HTTPException(
status_code=404, detail="Driver and constructor standings not found."
)
elif driver_standings_available:
# only driver standings are available
raise HTTPException(status_code=404, detail="Constructor standings not found.")
elif constructor_standings_available:
# only constructor standings are available
raise HTTPException(status_code=404, detail="Driver standings not found.")
else:
# something went wrong, investigate
raise HTTPException(
status_code=500, detail="Something went wrong. Investigate!"
)
54 changes: 53 additions & 1 deletion app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class ReadRoot(BaseModel):


class Schedule(BaseModel):
"""Model to store schedule data for a Formula 1 calendar year."""
"""Response model for schedule data for a Formula 1 calendar year."""

RoundNumber: int
Country: str
Expand Down Expand Up @@ -39,3 +39,55 @@ class HealthCheck(BaseModel):
"""Response model to validate and return when performing a health check."""

status: str = "OK"


class Driver(BaseModel):
"""Model for storing driver data"""

driverId: str
permanentNumber: str
code: str
url: str
givenName: str
familyName: str
dateOfBirth: str
nationality: str


class Contructor(BaseModel):
"""Model for storing constructor data"""

constructorId: str
url: str
name: str
nationality: str


class DriverStandings(BaseModel):
"""Model for storing driver standings data"""

position: str
positionText: str
points: str
wins: str
Driver: Driver
Constructors: list[Contructor]


class ConstructorStandings(BaseModel):
"""Model for storing constructor standings data"""

position: str
positionText: str
points: str
wins: str
Constructor: Contructor


class Standings(BaseModel):
"""Response model for driver and contructor standings for a given season and round"""

season: int
round: int
DriverStandings: list[DriverStandings]
ConstructorStandings: list[ConstructorStandings]
Loading

0 comments on commit 820ecb1

Please sign in to comment.