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

0.7.0 #73

Merged
merged 8 commits into from
May 9, 2024
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
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
SECRET_TOKEN=my-secret-token
FRONTEND_URL=http://localhost:3000
FRONTEND_DEV_URL=https://dev.slicktelemetry.com
FRONTEND_STAGING_URL=https://staging.slicktelemetry.com
FRONTEND_PROD_URL=https://slicktelemetry.com
10 changes: 5 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
repos:
# Execute external scripts
- repo: https://github.com/commitizen-tools/commitizen
rev: v3.24.0
rev: v3.25.0
hooks:
- id: commitizen
stages: [commit-msg]
Expand All @@ -11,28 +11,28 @@ repos:
hooks:
- id: run-formatters
name: Run formatters
entry: poetry poe formatters
entry: poetry run poe formatters
language: system
pass_filenames: false
stages: [pre-commit, pre-push]
files: ^(app/|poetry.lock|run.py)
- id: run-linters
name: Run linters
entry: poetry poe linters
entry: poetry run poe linters
language: system
pass_filenames: false
stages: [pre-commit, pre-push]
files: ^(app/|poetry.lock|run.py)
- id: run-typings
name: Run typings
entry: poetry poe typings
entry: poetry run poe typings
language: system
pass_filenames: false
stages: [pre-commit, pre-push]
files: ^(app/|poetry.lock|run.py)
# - id: run-tests
# name: Run tests
# entry: poetry poe tests
# entry: poetry run poe tests
# language: system
# pass_filenames: false
# stages: [pre-push]
Expand Down
2 changes: 1 addition & 1 deletion .python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.12.2
3.12.3
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
python 3.12.3
2 changes: 1 addition & 1 deletion Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.12.2-slim
FROM python:3.12.3-slim

# Set the working directory to /code.
WORKDIR /code
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile.prod
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# https://fastapi.tiangolo.com/deployment/docker/#docker-image-with-poetry

# First stage
FROM python:3.12.2-slim as requirements-stage
FROM python:3.12.3-slim as requirements-stage

# Set /tmp as the current working directory.
WORKDIR /tmp
Expand All @@ -16,7 +16,7 @@ COPY ./pyproject.toml ./poetry.lock* /tmp/
RUN poetry export -f requirements.txt --output requirements.txt --without-hashes

# This is the final stage, anything here will be preserved in the final container image.
FROM python:3.12.2-slim
FROM python:3.12.3-slim

# Install gcc and other dependencies
RUN apt-get update && apt-get install -y \
Expand Down
18 changes: 8 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Table of Contents:
- [Python virtual environment](#python-virtual-environment)
- [Installing dependencies](#installing-dependencies)
- [Virtual environment sanity check](#virtual-environment-sanity-check)
- [Poe the Poet poetry plugin](#poe-the-poet-poetry-plugin)
- [Poe the Poet](#poe-the-poet)
- [Installing git hooks](#installing-git-hooks)
- [New Relic integration](#new-relic-integration)
- [Running the project](#running-the-project)
Expand Down Expand Up @@ -61,29 +61,27 @@ poetry install --sync --no-root --with dev,lint,test
- Execute the `/Scripts/Activate` script from the virtual environment located [here](https://python-poetry.org/docs/configuration/#cache-directory).


#### Poe the Poet poetry plugin
#### Poe the Poet

```sh
poetry self add 'poethepoet[poetry_plugin]'
```
https://github.com/nat-n/poethepoet

1. Check available tasks:
```sh
poetry poe
poetry run poe
```
2. Execute a task:
```sh
poetry poe <task-name>
poetry run poe <task-name>
```
For example, running the project formatters:
```sh
poetry poe formatters
poetry run poe formatters
```

#### Installing git hooks

```sh
poetry poe git-hooks-setup
poetry run poe git-hooks-setup
```

### New Relic integration
Expand All @@ -100,7 +98,7 @@ poetry poe git-hooks-setup
### Running tests

```sh
poetry poe tests
poetry run poe tests
```

### Docker
Expand Down
8 changes: 7 additions & 1 deletion app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@
# Ergast configuration
ergast = Ergast(result_type="raw", auto_cast=True)
# Cors Middleware
origins = [config["FRONTEND_URL"], "http://127.0.0.1:3000"]
origins = [
"http://localhost:3000",
"http://127.0.0.1:3000",
config["FRONTEND_DEV_URL"],
config["FRONTEND_STAGING_URL"],
config["FRONTEND_PROD_URL"],
]
# Others
favicon_path = "favicon.ico"
# Security
Expand Down
102 changes: 101 additions & 1 deletion app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from typing import Annotated, List

# External
import pandas as pd
from fastapi import Depends, FastAPI, HTTPException, Path, Query, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
Expand All @@ -40,6 +41,7 @@
from .models import (
EventSchedule,
ExtendedTelemetry,
FastestSectors,
HealthCheck,
Laps,
Results,
Expand Down Expand Up @@ -482,7 +484,7 @@ def get_laps(
return session_laps_as_json_obj
except ValueError as ve:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Bad Request. {str(ve)}")
except KeyError as ke:
except KeyError:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Likely an error when fetching laps data for a session that has yet to happen.",
Expand Down Expand Up @@ -597,6 +599,104 @@ def get_split_qualifying_laps(
)


@newrelic.agent.web_transaction()
@app.get(
"/fastest-sectors/{year}/{round}",
tags=["laps"],
summary="Get fastest sectors of one or more drivers for a given year and round",
response_description="Return fastest sectors of one or more drivers for a given year and round.",
status_code=status.HTTP_200_OK,
response_model=FastestSectors,
dependencies=[Depends(validate_token)],
)
def get_fastest_sectors(
year: Annotated[
int,
Path(
title="The year for which to get the fastest sectors",
description="The year for which to get the fastest sectors",
ge=MIN_YEAR_SUPPORTED,
le=MAX_YEAR_SUPPORTED,
),
],
round: Annotated[
int,
Path(
title="The round in a year for which to get the fastest sectors",
description="The round in a year for which to get the fastest sectors",
ge=MIN_ROUND_SUPPORTED,
le=MAX_ROUND_SUPPORTED,
),
],
session: Annotated[
int,
Query(
title="The session in a round for which to get the fastest sectors",
description="The session in a round for which to get the fastest sectors. (Default = 5; ie race)",
ge=MIN_SESSION_SUPPORTED,
le=MAX_SESSION_SUPPORTED,
),
] = DEFAULT_SESSION,
driver_number: Annotated[
List[int],
Query(
title="List of drivers for whom to get the fastest sectors",
description="List of drivers for whom to get the fastest sectors",
),
] = [],
) -> FastestSectors:
"""
## Get fastest sectors of one or more drivers for a given year, round and session
Endpoint to get fastest sectors of one or more drivers for a given year, round and session.

**NOTE**:
- If `session` is not provided; we use the default session. Default = 5; ie race.
- If no `driver_number` are provided; you get fastest sectors for all drivers.

**Returns**:
FastestSectors: Returns a JSON response with the fastest sectors
"""

try:
session_obj = fastf1.get_session(year=year, gp=round, identifier=session)
session_obj.load(
laps=True,
telemetry=False,
weather=False,
messages=False,
)
session_laps = session_obj.laps

if len(driver_number) > 0:
session_laps = session_laps.pick_drivers(driver_number)

# Extract sector times per driver
sector1_times = session_laps.groupby("Driver")["Sector1Time"].min()
sector2_times = session_laps.groupby("Driver")["Sector2Time"].min()
sector3_times = session_laps.groupby("Driver")["Sector3Time"].min()

# Create a dictionary to hold the minimum sector times per driver
min_sector_times = {
driver: {
"MinSector1Time": sector1_times[driver].total_seconds() if pd.notna(sector1_times[driver]) else None,
"MinSector2Time": sector2_times[driver].total_seconds() if pd.notna(sector2_times[driver]) else None,
"MinSector3Time": sector3_times[driver].total_seconds() if pd.notna(sector3_times[driver]) else None,
}
for driver in session_laps["Driver"].unique()
}

# Output the results as JSON
return FastestSectors.model_validate(min_sector_times)
except ValueError as ve:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Bad Request. {str(ve)}")
except KeyError:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Likely an error when fetching laps data for a session that has yet to happen.",
)


@newrelic.agent.web_transaction()
@app.get(
"/telemetry/{year}/{round}/{driver_number}/{lap}",
tags=["telemetry"],
Expand Down
16 changes: 14 additions & 2 deletions app/models.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Built-in
from typing import List
from typing import Dict, List

# External
from pydantic import BaseModel
from pydantic import BaseModel, RootModel


class Root(BaseModel):
Expand Down Expand Up @@ -207,3 +207,15 @@ class ExtendedTelemetry(BaseModel):

Telemetry: List[Telemetry]
Weather: Weather | None


class DriverSectorTimes(BaseModel):
MinSector1Time: float
MinSector2Time: float
MinSector3Time: float


class FastestSectors(RootModel):
"""Response model for fastest sectors for a given year, round and session"""

Dict[str, DriverSectorTimes]
Loading