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

updates to schedule #11

Merged
merged 6 commits into from
Jan 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
1 change: 0 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"recommendations": [
"aaron-bond.better-comments",
"ms-python.black-formatter",
"ms-vscode-remote.remote-containers",
"ms-azuretools.vscode-docker",
"tamasfe.even-better-toml",
"kisstkondoros.vscode-gutter-preview",
Expand Down
10 changes: 5 additions & 5 deletions Dockerfile.dev
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,16 @@ RUN pip install --no-cache-dir poetry

# Use Poetry to install dependencies
RUN poetry config virtualenvs.create false \
&& poetry install --no-interaction --no-ansi
&& poetry install --no-interaction --no-ansi --no-root

# Make port 80 available to the world outside this container
EXPOSE 80
# Make port 8080 available to the world outside this container
EXPOSE 8080

# Healthcheck
HEALTHCHECK --interval=5m --timeout=3s \
CMD curl -f http://localhost/health || exit 1

# Run the uvicorn command, telling it to use the app object imported from app.main.
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80", "--reload"]
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080", "--reload"]

# CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80" "--reload"]
# CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "8080" "--reload"]
47 changes: 35 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ Table of Contents:

- [Setting up the project](#setting-up-the-project)
- [What you'll need](#what-youll-need)
- [Installing dependencies](#installing-dependencies)
- [Development](#development)
- [Running the project](#running-the-project)
- [Python virtual environment](#python-virtual-environment)
- [Installing dependencies](#installing-dependencies)
- [Running the project](#running-the-project)
- [Docker](#docker)
- [Interactive API docs](#interactive-api-docs)
- [Interactive Jupyter notebook](#interactive-jupyter-notebook)
- [Using Jupyter Lab](#using-jupyter-lab)
Expand All @@ -23,31 +25,52 @@ Table of Contents:
### What you'll need

- [VSCode](https://code.visualstudio.com/) / [Intellij Pycharm](https://www.jetbrains.com/pycharm/)
- [Python 3.11](https://www.python.org/) (Please check `pyproject.toml` for the latest supported python version)
- [Python 3.12](https://www.python.org/) (Please check [`pyproject.toml`](./pyproject.toml) for the latest supported python version)
- [Poetry](https://python-poetry.org/docs/#installing-with-the-official-installer) for dependency management
- [Docker Desktop](https://docs.docker.com/desktop/) [OPTIONAL]

### Installing dependencies
## Development

### Python virtual environment

#### Installing dependencies

```sh
poetry install --no-root
```

**Note**: Ensure that the python intepreter in your IDE is set to the newly created virtual environment by poetry. If you have not modified poetry configuration, you can find the virtual environment location as stated [here](https://python-poetry.org/docs/configuration/#cache-directory).

## Development

### Running the project
#### Running the project

- Open up a terminal in your IDE.
- Your IDE should activate the virtual environment for you automatically.
- If it doesnt, you can execute the `/Scripts/Activate` script from the virtual environment.
- If it doesnt, you can follow either of these steps:
- Set poetry [python interpreter path in VS Code](https://code.visualstudio.com/docs/python/environments#_working-with-python-interpreters) <u> ***OR*** </u>
- Run `poetry shell` <u> ***OR*** </u>
- Execute the `/Scripts/Activate` script from the virtual environment located [here](https://python-poetry.org/docs/configuration/#cache-directory)

- Run `python run.py` to start the server.
- Open your browser at `http://0.0.0.0/80`.
- Open your browser at `http://127.0.0.1/8080`.

### Docker

- Build image
```sh
docker build --file Dockerfile.dev --tag backend-dev .
```
**Note**: The first build will take about 10 minutes. Please be patient. Subsequent builds should be quicker (given that the image has not been prunes).
- Run container
```sh
docker run --detach --name backend-dev-container --publish 8080:8080 backend-dev
```
- Open your browser at http://127.0.0.1/8080.
- For other docker commands, see [useful_commands.md](./useful_commands.md)

### Interactive API docs

- Once the server is running, open your browser at `http://0.0.0.0:80/docs`.
- Alternate docs can be found at `http://0.0.0.0:80/redoc`, provided by [redoc](https://github.com/Redocly/redoc).
- Once the server is running, open your browser at `http://127.0.0.1:8080/docs`.
- Alternate docs can be found at `http://127.0.0.1:8080/redoc`, provided by [redoc](https://github.com/Redocly/redoc).

### Interactive Jupyter notebook

Expand All @@ -58,7 +81,7 @@ poetry install --no-root

#### Running the notebook in VS Code

- Alternatively, you can install `https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter` and run the notebook in VS Code.
- 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.

### Contribution Guidelines
Expand Down
21 changes: 13 additions & 8 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import json
from datetime import datetime
from typing import Annotated

import fastf1
from fastapi import FastAPI, Path, status
from fastapi import FastAPI, Query, status

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

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

app = FastAPI(
title="Slick Telemetry API",
Expand Down Expand Up @@ -58,30 +60,33 @@ def get_health() -> HealthCheck:
return HealthCheck(status="OK")


# TODO make {year} optional
@app.get(
"/schedule/{year}",
"/schedule",
tags=["schedule"],
summary="Get events schedule for a Formula 1 calendar year",
response_description="Return list of events schedule",
response_description="Return list of events schedule for a Formula 1 calendar year",
status_code=status.HTTP_200_OK,
response_model=list[Schedule],
)
def get_schedule(
year: Annotated[
int,
Path(
int | None,
Query(
title="The year for which to get the schedule",
gt=1949, # Supported years are 1950 to current
le=datetime.today().year,
),
]
] = None
) -> list[Schedule]:
"""
## Get events schedule for a Formula 1 calendar year
Endpoint to get events schedule for Formula 1 calendar year.
Returns:
list[Schedule]: Returns a JSON response with the list of event schedule
"""
if year is None:
year = get_default_year_for_schedule()

event_schedule = fastf1.get_event_schedule(year)

# Convert timestamp(z) related columns' data into a string type
Expand Down
43 changes: 40 additions & 3 deletions app/test_main.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from datetime import datetime

from fastapi.testclient import TestClient

from app.main import app
Expand All @@ -18,7 +20,7 @@ def test_healthcheck():


def test_get_schedule():
response = client.get("/schedule/2023")
response = client.get("/schedule")
assert response.status_code == 200
assert response.json()[0] == {
"RoundNumber": 0,
Expand Down Expand Up @@ -47,18 +49,53 @@ def test_get_schedule():
}


def test_get_schedule_bad_year_no_input():
response = client.get("/schedule/?year=")
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "int_parsing",
"loc": ["query", "year"],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "",
"url": "https://errors.pydantic.dev/2.5/v/int_parsing",
}
]
}


def test_get_schedule_bad_year_lower_limit():
response = client.get("/schedule/1949")
response = client.get("/schedule/?year=1949")
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "greater_than",
"loc": ["path", "year"],
"loc": ["query", "year"],
"msg": "Input should be greater than 1949",
"input": "1949",
"ctx": {"gt": 1949},
"url": "https://errors.pydantic.dev/2.5/v/greater_than",
}
]
}


def test_get_schedule_bad_year_upper_limit():
current_year = datetime.today().year
bad_year = current_year + 1
response = client.get(f"/schedule/?year={bad_year}")
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "less_than_equal",
"loc": ["query", "year"],
"msg": "Input should be less than or equal to 2024",
"input": "2025",
"ctx": {"le": 2024},
"url": "https://errors.pydantic.dev/2.5/v/less_than_equal",
}
]
}
24 changes: 24 additions & 0 deletions app/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from datetime import datetime

import fastf1


def get_default_year_for_schedule() -> int:
# default year is defined as the year which has data for atleast 1 race session

current_year = datetime.today().year
event_schedule = fastf1.get_event_schedule(current_year, include_testing=False)

if len(event_schedule) == 0:
return current_year - 1

try:
first_event_round_number = event_schedule.iloc[0]["RoundNumber"]
first_event_session = fastf1.get_session(
current_year, first_event_round_number, "Race"
)
first_event_session.load()
except (ValueError, KeyError):
return current_year - 1

return current_year - 1
Loading