implemented architecture for LLM to dynamically create approprite API requests to OpenF1 server
Browse files- openf1_api_playground.ipynb +0 -0
- openf1_registry.py +208 -0
- openf1_tools.py +169 -0
- tools.py +0 -243
- utils/request_utils.py +0 -1
openf1_api_playground.ipynb
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
openf1_registry.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from enum import Enum
|
| 2 |
+
from dataclasses import dataclass
|
| 3 |
+
from typing import Dict, Set, List, Optional
|
| 4 |
+
|
| 5 |
+
class FilterType(Enum):
|
| 6 |
+
EQUALITY = "equality" # exact match
|
| 7 |
+
COMPARISON = "comparison" # >, <, >=, <=
|
| 8 |
+
|
| 9 |
+
class DataType(Enum):
|
| 10 |
+
STRING = "string"
|
| 11 |
+
INTEGER = "integer"
|
| 12 |
+
DATETIME = "datetime"
|
| 13 |
+
BINARY = "binary" # true/false filters
|
| 14 |
+
|
| 15 |
+
@dataclass
|
| 16 |
+
class FilterSpec:
|
| 17 |
+
"""Specification for an API filter parameter"""
|
| 18 |
+
name: str
|
| 19 |
+
filter_type: FilterType
|
| 20 |
+
data_type: DataType
|
| 21 |
+
description: str = ""
|
| 22 |
+
allowed_values: Optional[List[str]] = None # For equality filters with restricted values
|
| 23 |
+
|
| 24 |
+
def get_query_examples(self) -> List[str]:
|
| 25 |
+
"""Generate example query parameters for this filter"""
|
| 26 |
+
examples = []
|
| 27 |
+
|
| 28 |
+
if self.data_type == DataType.BINARY:
|
| 29 |
+
examples = [f"{self.name}=true", f"{self.name}=false"]
|
| 30 |
+
|
| 31 |
+
elif self.filter_type == FilterType.EQUALITY:
|
| 32 |
+
if self.allowed_values:
|
| 33 |
+
examples = [f"{self.name}={val}" for val in self.allowed_values[:2]]
|
| 34 |
+
elif self.data_type == DataType.STRING:
|
| 35 |
+
examples = [f"{self.name}=example_value"]
|
| 36 |
+
elif self.data_type == DataType.INTEGER:
|
| 37 |
+
examples = [f"{self.name}=42"]
|
| 38 |
+
elif self.data_type == DataType.DATETIME:
|
| 39 |
+
examples = [f"{self.name}=2024-01-01T00:00:00Z", f"{self.name}=2024-01-01T10:30:00Z"]
|
| 40 |
+
|
| 41 |
+
elif self.filter_type == FilterType.COMPARISON:
|
| 42 |
+
if self.data_type == DataType.INTEGER:
|
| 43 |
+
examples = [f"{self.name}>=10", f"{self.name}<100"]
|
| 44 |
+
elif self.data_type == DataType.DATETIME:
|
| 45 |
+
examples = [f"{self.name}>=2024-01-01T00:00:00Z", f"{self.name}<2024-12-31T00:00:00Z"]
|
| 46 |
+
elif self.data_type == DataType.STRING:
|
| 47 |
+
examples = [f"{self.name}>M", f"{self.name}<Z"] # alphabetical comparison
|
| 48 |
+
|
| 49 |
+
return examples
|
| 50 |
+
|
| 51 |
+
def help_text(self) -> str:
|
| 52 |
+
"""Generate help text for this filter"""
|
| 53 |
+
text = f"Filter: {self.name}\n"
|
| 54 |
+
text += f" Type: {self.filter_type.value} ({self.data_type.value})\n"
|
| 55 |
+
|
| 56 |
+
if self.description:
|
| 57 |
+
text += f" Description: {self.description}\n"
|
| 58 |
+
|
| 59 |
+
if self.allowed_values:
|
| 60 |
+
text += f" Allowed values: {', '.join(self.allowed_values)}\n"
|
| 61 |
+
|
| 62 |
+
examples = self.get_query_examples()
|
| 63 |
+
if examples:
|
| 64 |
+
text += f" Examples: {', '.join(examples)}"
|
| 65 |
+
|
| 66 |
+
return text
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class APIEndpointRegistry:
|
| 71 |
+
"""Registry for API endpoints and their supported filters"""
|
| 72 |
+
|
| 73 |
+
def __init__(self, base_url: str = ""):
|
| 74 |
+
self.base_url = base_url
|
| 75 |
+
self.endpoints: Dict[str, Set[str]] = {} # endpoint -> filter names
|
| 76 |
+
self.filters: Dict[str, FilterSpec] = {} # global filter definitions
|
| 77 |
+
|
| 78 |
+
def define_filter(
|
| 79 |
+
self,
|
| 80 |
+
name: str,
|
| 81 |
+
filter_type: FilterType,
|
| 82 |
+
data_type: DataType,
|
| 83 |
+
description: str = "",
|
| 84 |
+
allowed_values: Optional[List[str]] = None
|
| 85 |
+
) -> 'APIEndpointRegistry':
|
| 86 |
+
"""Define a filter that can be used by endpoints"""
|
| 87 |
+
filter_spec = FilterSpec(name, filter_type, data_type, description, allowed_values)
|
| 88 |
+
self.filters[name] = filter_spec
|
| 89 |
+
return self
|
| 90 |
+
|
| 91 |
+
def register_endpoint(self, endpoint: str, *filter_names: str) -> 'APIEndpointRegistry':
|
| 92 |
+
"""Register an API endpoint with its supported filters"""
|
| 93 |
+
|
| 94 |
+
# Validate all filters exist
|
| 95 |
+
for filter_name in filter_names:
|
| 96 |
+
if filter_name not in self.filters:
|
| 97 |
+
raise ValueError(f"Filter '{filter_name}' not defined. Use define_filter() first.")
|
| 98 |
+
|
| 99 |
+
# Store endpoint -> filters mapping
|
| 100 |
+
self.endpoints[endpoint] = set(filter_names)
|
| 101 |
+
|
| 102 |
+
return self
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def get_endpoint_filters(self, endpoint: str) -> Dict[str, FilterSpec]:
|
| 109 |
+
"""Get all filters supported by an endpoint"""
|
| 110 |
+
filter_names = self.endpoints.get(endpoint, set())
|
| 111 |
+
return {name: self.filters[name] for name in filter_names}
|
| 112 |
+
|
| 113 |
+
def get_filter_help(self, filter_name: str) -> str:
|
| 114 |
+
"""Get help text for a specific filter"""
|
| 115 |
+
|
| 116 |
+
if filter_name in self.filters:
|
| 117 |
+
return self.filters[filter_name].help_text()
|
| 118 |
+
return f"Filter '{filter_name}' not found."
|
| 119 |
+
|
| 120 |
+
def get_endpoint_help(self, endpoint: str) -> str:
|
| 121 |
+
"""Get help text for all filters supported by an endpoint"""
|
| 122 |
+
|
| 123 |
+
filters = self.get_endpoint_filters(endpoint)
|
| 124 |
+
if not filters:
|
| 125 |
+
return f"Endpoint '{endpoint}' has no registered filters."
|
| 126 |
+
|
| 127 |
+
help_text = f"API Endpoint: {self.base_url}{endpoint}\n"
|
| 128 |
+
help_text += f"Supported filters ({len(filters)}):\n\n"
|
| 129 |
+
|
| 130 |
+
for name, spec in sorted(filters.items()):
|
| 131 |
+
help_text += spec.help_text() + "\n\n"
|
| 132 |
+
|
| 133 |
+
return help_text.strip()
|
| 134 |
+
|
| 135 |
+
def list_all_endpoints(self) -> List[str]:
|
| 136 |
+
"""Get list of all registered endpoints"""
|
| 137 |
+
return sorted(self.endpoints.keys())
|
| 138 |
+
|
| 139 |
+
def list_all_filters(self) -> List[str]:
|
| 140 |
+
"""Get list of all defined filters"""
|
| 141 |
+
return sorted(self.filters.keys())
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
# Create registry with base URL
|
| 145 |
+
f1_api = APIEndpointRegistry("https://api.openf1.org/v1/")
|
| 146 |
+
|
| 147 |
+
# Define filters with their specifications
|
| 148 |
+
f1_api.define_filter("date", FilterType.COMPARISON, DataType.DATETIME, "The UTC date and time, in ISO 8601 format.")
|
| 149 |
+
f1_api.define_filter("driver_number", FilterType.EQUALITY, DataType.INTEGER, "The unique number assigned to an F1 driver")
|
| 150 |
+
f1_api.define_filter("meeting_key", FilterType.EQUALITY, DataType.STRING, "The unique identifier for the meeting. Use 'latest' to identify the latest or current meeting.")
|
| 151 |
+
f1_api.define_filter("session_key", FilterType.EQUALITY, DataType.STRING, "The unique identifier for the session. Use 'latest' to identify the latest or current session.")
|
| 152 |
+
f1_api.define_filter("speed", FilterType.COMPARISON, DataType.INTEGER, "Velocity of the car in km/h.")
|
| 153 |
+
f1_api.define_filter("country_code", FilterType.EQUALITY, DataType.STRING, "A code that uniquely identifies the country.")
|
| 154 |
+
f1_api.define_filter("first_name", FilterType.EQUALITY, DataType.STRING, "The first name of the driver.")
|
| 155 |
+
f1_api.define_filter("last_name", FilterType.EQUALITY, DataType.STRING, "The last name of the driver.")
|
| 156 |
+
f1_api.define_filter("full_name", FilterType.EQUALITY, DataType.STRING, "The full name of the driver.")
|
| 157 |
+
f1_api.define_filter("name_acronym", FilterType.EQUALITY, DataType.STRING, "Three-letter acronym of the driver's name.")
|
| 158 |
+
f1_api.define_filter("team_name", FilterType.EQUALITY, DataType.STRING, "The name of the driver's team.")
|
| 159 |
+
f1_api.define_filter("gap_to_leader", FilterType.COMPARISON, DataType.INTEGER, "The time gap to the race leader in seconds, +1 LAP if lapped, or null for the race leader.")
|
| 160 |
+
f1_api.define_filter("interval", FilterType.COMPARISON, DataType.INTEGER, "The time gap to the car ahead in seconds, +1 LAP if lapped, or null for the race leader.")
|
| 161 |
+
f1_api.define_filter("date_start", FilterType.COMPARISON, DataType.DATETIME, "The UTC starting date and time, in ISO 8601 format.")
|
| 162 |
+
f1_api.define_filter("date_end", FilterType.COMPARISON, DataType.DATETIME, "The UTC ending date and time, in ISO 8601 format.")
|
| 163 |
+
f1_api.define_filter("is_pit_out_lap", FilterType.EQUALITY, DataType.BINARY, "A boolean value indicating whether the lap is an out lap from the pit (true if it is, false otherwise).")
|
| 164 |
+
f1_api.define_filter("lap_duration", FilterType.COMPARISON, DataType.INTEGER, "The total time taken, in seconds, to complete the entire lap.")
|
| 165 |
+
f1_api.define_filter("lap_number", FilterType.EQUALITY, DataType.INTEGER, "The sequential number of the lap within the session (starts at 1).")
|
| 166 |
+
f1_api.define_filter("circuit_key", FilterType.EQUALITY, DataType.STRING, "The unique identifier for the circuit where the event takes place.")
|
| 167 |
+
f1_api.define_filter("circuit_short_name", FilterType.EQUALITY, DataType.STRING, "The short or common name of the circuit where the event takes place.")
|
| 168 |
+
f1_api.define_filter("country_key", FilterType.EQUALITY, DataType.STRING, "The unique identifier for the country where the event takes place.")
|
| 169 |
+
f1_api.define_filter("country_name", FilterType.EQUALITY, DataType.STRING, "The name of the country where the event takes place.")
|
| 170 |
+
f1_api.define_filter("location", FilterType.EQUALITY, DataType.STRING, "The city or geographical location where the event takes place.")
|
| 171 |
+
f1_api.define_filter("meeting_name", FilterType.EQUALITY, DataType.STRING, "The name of the meeting.")
|
| 172 |
+
f1_api.define_filter("meeting_official_name", FilterType.EQUALITY, DataType.STRING, "The official name of the meeting.")
|
| 173 |
+
f1_api.define_filter("year", FilterType.EQUALITY, DataType.INTEGER, "The year of the event.")
|
| 174 |
+
f1_api.define_filter("pit_duration", FilterType.COMPARISON, DataType.INTEGER, "The time spent in the pit, from entering to leaving the pit lane, in seconds.")
|
| 175 |
+
f1_api.define_filter("position", FilterType.EQUALITY, DataType.INTEGER, "Position of the driver (starts at 1).", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20])
|
| 176 |
+
f1_api.define_filter("category", FilterType.EQUALITY, DataType.STRING, "The category of the event (CarEvent, Drs, Flag, SafetyCar)", ["CarEvent", "Drs", "Flag", "SafetyCar"])
|
| 177 |
+
f1_api.define_filter("flag", FilterType.EQUALITY, DataType.STRING, "The flag displayed to the drivers.", ["Green", "Yellow", "Red", "Black", "White", "Blue", "Checkered", "White"])
|
| 178 |
+
f1_api.define_filter("message", FilterType.EQUALITY, DataType.STRING, "Description of the event or action.")
|
| 179 |
+
f1_api.define_filter("session_name", FilterType.EQUALITY, DataType.STRING, "The name of the session (Practice 1, Qualifying, Race, ...).")
|
| 180 |
+
f1_api.define_filter("session_type", FilterType.EQUALITY, DataType.STRING, "The type of the session (Practice, Qualifying, Race, ...).")
|
| 181 |
+
f1_api.define_filter("compound", FilterType.EQUALITY, DataType.STRING, "The specific compound of tyre used during the stint (SOFT, MEDIUM, HARD, ...).", ["SOFT", "MEDIUM", "HARD", "INTERMEDIATE", "WET"])
|
| 182 |
+
f1_api.define_filter("lap_end", FilterType.COMPARISON, DataType.INTEGER, "Number of the last completed lap in this stint.")
|
| 183 |
+
f1_api.define_filter("lap_start", FilterType.COMPARISON, DataType.INTEGER, "Number of the initial lap in this stint (starts at 1).")
|
| 184 |
+
f1_api.define_filter("stint_number", FilterType.EQUALITY, DataType.INTEGER, "The sequential number of the stint within the session (starts at 1).")
|
| 185 |
+
f1_api.define_filter("tyre_age_at_start", FilterType.COMPARISON, DataType.INTEGER, "The age of the tyres at the start of the stint, in laps completed.")
|
| 186 |
+
f1_api.define_filter("air_temperature", FilterType.COMPARISON, DataType.INTEGER, "Air temperature (°C).")
|
| 187 |
+
f1_api.define_filter("humidity", FilterType.COMPARISON, DataType.INTEGER, "Humidity percentage.")
|
| 188 |
+
f1_api.define_filter("pressure", FilterType.COMPARISON, DataType.INTEGER, "Air pressure (mbar).")
|
| 189 |
+
f1_api.define_filter("rainfall", FilterType.COMPARISON, DataType.INTEGER, "Whether there is rainfall.")
|
| 190 |
+
f1_api.define_filter("track_temperature", FilterType.COMPARISON, DataType.INTEGER, "Track temperature (°C).")
|
| 191 |
+
f1_api.define_filter("wind_direction", FilterType.COMPARISON, DataType.INTEGER, "Wind direction (°), from 0° to 359°.")
|
| 192 |
+
f1_api.define_filter("wind_speed", FilterType.COMPARISON, DataType.INTEGER, "Wind speed (m/s).")
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
# Register API endpoints with their supported filters
|
| 196 |
+
f1_api.register_endpoint("car_data", "date", "driver_number", "meeting_key", "session_key", "speed")
|
| 197 |
+
f1_api.register_endpoint("drivers", "session_key", "meeting_key", "country_code", "driver_number", "first_name", "last_name", "full_name", "name_acronym", "team_name")
|
| 198 |
+
f1_api.register_endpoint("intervals", "date", "driver_number", "meeting_key", "session_key", "gap_to_leader", "interval")
|
| 199 |
+
f1_api.register_endpoint("laps", "date_start", "driver_number", "meeting_key", "session_key", "lap_duration", "lap_number", "is_pit_out_lap")
|
| 200 |
+
f1_api.register_endpoint("location", "date", "driver_number", "meeting_key", "session_key")
|
| 201 |
+
f1_api.register_endpoint("meetings", "circuit_key", "circuit_short_name", "country_code", "country_key", "country_name", "date_start", "location", "meeting_key", "meeting_name", "meeting_official_name", "year")
|
| 202 |
+
f1_api.register_endpoint("pit", "date", "driver_number", "lap_number", "meeting_key", "session_key", "pit_duration")
|
| 203 |
+
f1_api.register_endpoint("position", "date", "driver_number", "meeting_key", "session_key", "position")
|
| 204 |
+
f1_api.register_endpoint("race_control", "category", "date", "driver_number", "meeting_key", "session_key", "flag", "message", "lap_number")
|
| 205 |
+
f1_api.register_endpoint("sessions", "circuit_key", "circuit_short_name", "country_code", "country_key", "country_name", "date_start", "date_end", "location", "session_name", "session_type", "session_key", "meeting_key", "year")
|
| 206 |
+
f1_api.register_endpoint("stints", "compound", "driver_number", "lap_end", "lap_start", "meeting_key", "session_key", "stint_number", "tyre_age_at_start")
|
| 207 |
+
f1_api.register_endpoint("team_radio", "date", "driver_number", "meeting_key", "session_key")
|
| 208 |
+
f1_api.register_endpoint("weather", "air_temperature", "date", "humidity", "pressure", "rainfall", "track_temperature", "wind_direction", "wind_speed", "meeting_key", "session_key")
|
openf1_tools.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from urllib.request import urlopen
|
| 3 |
+
from openf1_registry import f1_api
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
api_endpoints = {
|
| 7 |
+
"sessions": "sessions?",
|
| 8 |
+
"weather": "weather?",
|
| 9 |
+
"locations": "locations?",
|
| 10 |
+
"drivers": "drivers?",
|
| 11 |
+
"intervals": "intervals?",
|
| 12 |
+
"laps": "laps?",
|
| 13 |
+
"car_data": "car_data?",
|
| 14 |
+
"pit": "pit?",
|
| 15 |
+
"position": "position?",
|
| 16 |
+
"race_control": "race_control?",
|
| 17 |
+
"stints": "stints?",
|
| 18 |
+
"team_radio": "team_radio?",
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
### Essential tools ###
|
| 23 |
+
|
| 24 |
+
def get_api_endpoint(endpoint: str) -> dict:
|
| 25 |
+
"""
|
| 26 |
+
Retrieve the API endpoint URL and filter metadata for a given OpenF1 endpoint.
|
| 27 |
+
|
| 28 |
+
Args:
|
| 29 |
+
endpoint (str): The name of the OpenF1 API endpoint (e.g., 'sessions', 'laps').
|
| 30 |
+
|
| 31 |
+
Returns:
|
| 32 |
+
dict: A dictionary containing:
|
| 33 |
+
- status (str): 'success' if endpoint exists, 'error' otherwise
|
| 34 |
+
- api_string (str): The full API URL for the endpoint
|
| 35 |
+
- filter_metadata (dict): Available filters for the endpoint
|
| 36 |
+
"""
|
| 37 |
+
try:
|
| 38 |
+
url = f1_api.base_url + api_endpoints.get(endpoint, None)
|
| 39 |
+
return {
|
| 40 |
+
"status": "success",
|
| 41 |
+
"api_string": url,
|
| 42 |
+
"filter_metadata": f1_api.get_endpoint_filters(endpoint)
|
| 43 |
+
}
|
| 44 |
+
except:
|
| 45 |
+
return {
|
| 46 |
+
"status": "error",
|
| 47 |
+
"api_string": f"Endpoint {endpoint} not found. Available endpoints: {f1_api.list_all_endpoints()}",
|
| 48 |
+
"filter_metadata": dict()
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
def get_filter_string(filter_name: str, filter_value: str, operator: str = "=") -> str:
|
| 52 |
+
"""
|
| 53 |
+
Create a filter string for OpenF1 API requests.
|
| 54 |
+
|
| 55 |
+
Args:
|
| 56 |
+
filter_name (str): The name of the filter to apply.
|
| 57 |
+
filter_value (str): The value to filter by.
|
| 58 |
+
operator (str, optional): The comparison operator. Defaults to "=".
|
| 59 |
+
|
| 60 |
+
Returns:
|
| 61 |
+
str: Formatted filter string that can be appended to an API request.
|
| 62 |
+
"""
|
| 63 |
+
return f"{filter_name}{operator}{filter_value}&"
|
| 64 |
+
|
| 65 |
+
def apply_filters(api_string: str, *filters: str) -> str:
|
| 66 |
+
"""
|
| 67 |
+
Apply one or more filter strings to an API endpoint URL.
|
| 68 |
+
|
| 69 |
+
Args:
|
| 70 |
+
api_string (str): The base API endpoint URL.
|
| 71 |
+
*filters (str): Variable number of filter strings to apply.
|
| 72 |
+
|
| 73 |
+
Returns:
|
| 74 |
+
str: The complete API URL with all filters applied.
|
| 75 |
+
"""
|
| 76 |
+
if filters:
|
| 77 |
+
for filter in filters:
|
| 78 |
+
api_string += filter
|
| 79 |
+
return api_string.rstrip("&") # Remove trailing & for last filter
|
| 80 |
+
|
| 81 |
+
def send_request(api_string: str) -> dict:
|
| 82 |
+
"""
|
| 83 |
+
Send an HTTP GET request to the specified API endpoint and return the JSON response.
|
| 84 |
+
|
| 85 |
+
Args:
|
| 86 |
+
api_string (str): The complete API URL to send the request to.
|
| 87 |
+
|
| 88 |
+
Returns:
|
| 89 |
+
dict: The JSON response parsed as a Python dictionary.
|
| 90 |
+
|
| 91 |
+
Raises:
|
| 92 |
+
Exception: If there's an error during the HTTP request or JSON parsing.
|
| 93 |
+
"""
|
| 94 |
+
try:
|
| 95 |
+
response = urlopen(api_string)
|
| 96 |
+
data = json.loads(response.read().decode('utf-8'))
|
| 97 |
+
return data
|
| 98 |
+
except Exception as e:
|
| 99 |
+
print(f"Error: {e}")
|
| 100 |
+
raise
|
| 101 |
+
|
| 102 |
+
### LLM helper functions ###
|
| 103 |
+
|
| 104 |
+
def get_api_endpoints() -> dict:
|
| 105 |
+
"""
|
| 106 |
+
Retrieve a list of all available OpenF1 API endpoints.
|
| 107 |
+
|
| 108 |
+
Returns:
|
| 109 |
+
dict: A dictionary containing a single key 'endpoints' with a list of
|
| 110 |
+
available endpoint names as strings.
|
| 111 |
+
"""
|
| 112 |
+
return {
|
| 113 |
+
"endpoints": f1_api.list_all_endpoints(),
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
def get_endpoint_info(endpoint: str) -> dict:
|
| 117 |
+
"""
|
| 118 |
+
Retrieve detailed information about a specific OpenF1 API endpoint.
|
| 119 |
+
|
| 120 |
+
Args:
|
| 121 |
+
endpoint (str): The name of the endpoint to get information about.
|
| 122 |
+
|
| 123 |
+
Returns:
|
| 124 |
+
dict: A dictionary containing:
|
| 125 |
+
- endpoint (str): The name of the endpoint
|
| 126 |
+
- endpoint_filters (list): Available filters for this endpoint
|
| 127 |
+
- endpoint_help (str): Help text describing the endpoint's purpose and usage
|
| 128 |
+
"""
|
| 129 |
+
return {
|
| 130 |
+
"endpoint": endpoint,
|
| 131 |
+
"endpoint_filters": f1_api.get_endpoint_filters(endpoint),
|
| 132 |
+
"endpoint_help": f1_api.get_endpoint_help(endpoint)
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
def get_filter_info(filter_name: str) -> dict:
|
| 136 |
+
"""
|
| 137 |
+
Retrieve detailed information about a specific OpenF1 API filter.
|
| 138 |
+
|
| 139 |
+
Args:
|
| 140 |
+
filter_name (str): The name of the filter to get information about.
|
| 141 |
+
|
| 142 |
+
Returns:
|
| 143 |
+
dict: A dictionary containing:
|
| 144 |
+
- filter_name (str): The name of the filter
|
| 145 |
+
- filter_metadata (dict): Metadata about the filter, including
|
| 146 |
+
description, valid values, and usage examples
|
| 147 |
+
"""
|
| 148 |
+
return {
|
| 149 |
+
"filter_name": filter_name,
|
| 150 |
+
"filter_metadata": f1_api.get_filter_help(filter_name)
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
if __name__ == "__main__":
|
| 158 |
+
|
| 159 |
+
from pprint import pprint
|
| 160 |
+
|
| 161 |
+
data = get_api_endpoint("sessions")
|
| 162 |
+
pprint(data)
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
pprint(get_endpoint_info("sessions"))
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
# response = send_request(data["api_string"])
|
| 169 |
+
# pprint(response)
|
tools.py
DELETED
|
@@ -1,243 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
This module provides tools for interacting with Formula 1 data using the FastF1 library.
|
| 3 |
-
|
| 4 |
-
Tools to implement
|
| 5 |
-
- driver info
|
| 6 |
-
- compare drivers
|
| 7 |
-
"""
|
| 8 |
-
|
| 9 |
-
import json
|
| 10 |
-
import fastf1
|
| 11 |
-
import gradio as gr
|
| 12 |
-
import pandas as pd
|
| 13 |
-
from PIL import Image
|
| 14 |
-
from typing import Union
|
| 15 |
-
from fastf1.core import Session
|
| 16 |
-
|
| 17 |
-
# Local modules
|
| 18 |
-
from utils import parser_utils, track_utils
|
| 19 |
-
from utils.constants import (
|
| 20 |
-
AVAILABLE_SESSION_TYPES,
|
| 21 |
-
DRIVER_DETAILS
|
| 22 |
-
)
|
| 23 |
-
|
| 24 |
-
# Custom types
|
| 25 |
-
gp = Union[str, int]
|
| 26 |
-
session_type = Union[str, int, None]
|
| 27 |
-
|
| 28 |
-
### FastF1 tools ###
|
| 29 |
-
|
| 30 |
-
def get_session(year: int, round: gp, session_type: session_type) -> Session:
|
| 31 |
-
"""Retrieve a specific Formula 1 session.
|
| 32 |
-
|
| 33 |
-
Args:
|
| 34 |
-
year (int): The season year (e.g., 2024)
|
| 35 |
-
round (str | int): The race round number or name (e.g., 1 or 'Monaco')
|
| 36 |
-
session_type (str | int | None): Type of session (e.g., 'FP1', 'Q', 'R')
|
| 37 |
-
|
| 38 |
-
Returns:
|
| 39 |
-
Session: A FastF1 Session object for the specified parameters
|
| 40 |
-
|
| 41 |
-
Note:
|
| 42 |
-
If session_type is a string and not in AVAILABLE_SESSION_TYPES,
|
| 43 |
-
returns an error message string instead.
|
| 44 |
-
"""
|
| 45 |
-
|
| 46 |
-
# Check if session type is valid
|
| 47 |
-
if isinstance(session_type, str):
|
| 48 |
-
if session_type.lower() not in AVAILABLE_SESSION_TYPES:
|
| 49 |
-
return f"Session type {session_type} is not available. Supported session types: {AVAILABLE_SESSION_TYPES}"
|
| 50 |
-
|
| 51 |
-
return fastf1.get_session(year, round, session_type)
|
| 52 |
-
|
| 53 |
-
def get_season_calendar(year: int) -> str:
|
| 54 |
-
"""Get the complete race calendar for a specific F1 season.
|
| 55 |
-
|
| 56 |
-
Args:
|
| 57 |
-
year (int): The season year to get the calendar for
|
| 58 |
-
|
| 59 |
-
Returns:
|
| 60 |
-
str: Formatted string containing the season calendar
|
| 61 |
-
"""
|
| 62 |
-
season_calendar = fastf1.get_event_schedule(year)
|
| 63 |
-
return parser_utils.parse_season_calendar(season_calendar)
|
| 64 |
-
|
| 65 |
-
def get_event_info(year: int, round: gp, format: str) -> str:
|
| 66 |
-
"""Retrieve information about a specific Formula 1 event.
|
| 67 |
-
|
| 68 |
-
Args:
|
| 69 |
-
year (int): The season year
|
| 70 |
-
round (str | int): The race round number or name
|
| 71 |
-
format (str): Output format ('human' for readable text, 'LLM' for structured data)
|
| 72 |
-
|
| 73 |
-
Returns:
|
| 74 |
-
str: Formatted event information based on the specified format
|
| 75 |
-
"""
|
| 76 |
-
|
| 77 |
-
event = fastf1.get_session(year, round, "race").event # Event object is the same for all sessions, so hardcode "race"
|
| 78 |
-
if format == "human":
|
| 79 |
-
data_interval = f"{event['Session1DateUtc'].date()} - {event['Session5DateUtc'].date()}"
|
| 80 |
-
event_string = f"Round {event['RoundNumber']} : {event['EventName']} - {event['Location']}, {event['Country']} ({data_interval})"
|
| 81 |
-
return event_string
|
| 82 |
-
elif format == "LLM":
|
| 83 |
-
return parser_utils.parse_event_info(event)
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
def get_constructor_standings(year: int) -> str:
|
| 87 |
-
"""Retrieve constructor championship standings for a given year.
|
| 88 |
-
|
| 89 |
-
Args:
|
| 90 |
-
year (int): The season year
|
| 91 |
-
|
| 92 |
-
Returns:
|
| 93 |
-
str: Constructor championship standings
|
| 94 |
-
"""
|
| 95 |
-
pass
|
| 96 |
-
|
| 97 |
-
def get_driver_standings(year: int) -> str:
|
| 98 |
-
"""Retrieve driver championship standings for a given year.
|
| 99 |
-
|
| 100 |
-
Args:
|
| 101 |
-
year (int): The season year
|
| 102 |
-
|
| 103 |
-
Returns:
|
| 104 |
-
str: Driver championship standings
|
| 105 |
-
"""
|
| 106 |
-
pass
|
| 107 |
-
|
| 108 |
-
def driver_championship_standings(year: int, driver_name: str) -> str:
|
| 109 |
-
"""Get the championship standing for a specific driver in a given year.
|
| 110 |
-
|
| 111 |
-
Args:
|
| 112 |
-
year (int): The season year
|
| 113 |
-
driver_name (str): Full name of the driver (e.g., 'Lewis Hamilton')
|
| 114 |
-
|
| 115 |
-
Returns:
|
| 116 |
-
str: Formatted string with driver's position, points, and wins
|
| 117 |
-
"""
|
| 118 |
-
|
| 119 |
-
with open("assets/driver_abbreviations.json") as f:
|
| 120 |
-
driver_abbreviations = json.load(f)
|
| 121 |
-
driver_abbreviation = driver_abbreviations[driver_name]
|
| 122 |
-
ergast = fastf1.ergast.Ergast()
|
| 123 |
-
driver_standings = ergast.get_driver_standings(year).content[0]
|
| 124 |
-
driver_standing = driver_standings[["position", "points", "wins", "driverCode"]].reset_index(drop=True)
|
| 125 |
-
driver_standing = driver_standing[driver_standing["driverCode"] == driver_abbreviation]
|
| 126 |
-
suffix = "st" if driver_standing['position'].iloc[0] == 1 else "nd" if driver_standing['position'].iloc[0] == 2 else "rd" if driver_standing['position'].iloc[0] == 3 else "th"
|
| 127 |
-
standings_string = f"{driver_name} is {driver_standing['position'].iloc[0]}{suffix} with {driver_standing['points'].iloc[0]} points and {driver_standing['wins'].iloc[0]} wins"
|
| 128 |
-
return standings_string
|
| 129 |
-
|
| 130 |
-
def constructor_championship_standings(year: int, constructor_name: str) -> str:
|
| 131 |
-
"""Get the championship standing for a specific constructor in a given year.
|
| 132 |
-
|
| 133 |
-
Args:
|
| 134 |
-
year (int): The season year
|
| 135 |
-
constructor_name (str): Name of the constructor team (e.g., 'Mercedes')
|
| 136 |
-
|
| 137 |
-
Returns:
|
| 138 |
-
str: Formatted string with constructor's position, points, and wins
|
| 139 |
-
"""
|
| 140 |
-
|
| 141 |
-
team_mapping = {
|
| 142 |
-
"McLaren": "McLaren",
|
| 143 |
-
"Ferrari": "Ferrari",
|
| 144 |
-
"Red Bull Racing": "Red Bull",
|
| 145 |
-
"Mercedes": "Mercedes",
|
| 146 |
-
"Aston Martin": "Aston Martin",
|
| 147 |
-
"Alpine": "Alpine F1 Team",
|
| 148 |
-
"Haas": "Haas F1 Team",
|
| 149 |
-
"Racing Bulls": "RB F1 Team",
|
| 150 |
-
"Williams": "Williams",
|
| 151 |
-
"Kick Sauber": "Sauber"
|
| 152 |
-
}
|
| 153 |
-
|
| 154 |
-
ergast = fastf1.ergast.Ergast()
|
| 155 |
-
constructor_standings = ergast.get_constructor_standings(year).content[0]
|
| 156 |
-
constructor_standing = constructor_standings[["position", "points", "wins", "constructorName"]].reset_index(drop=True)
|
| 157 |
-
mapped_name = team_mapping[constructor_name]
|
| 158 |
-
constructor_standing = constructor_standing[constructor_standing["constructorName"] == mapped_name]
|
| 159 |
-
suffix = "st" if constructor_standing['position'].iloc[0] == 1 else "nd" if constructor_standing['position'].iloc[0] == 2 else "rd" if constructor_standing['position'].iloc[0] == 3 else "th"
|
| 160 |
-
standings_string = f"{constructor_name} are {constructor_standing['position'].iloc[0]}{suffix} with {constructor_standing['points'].iloc[0]} points and {constructor_standing['wins'].iloc[0]} wins"
|
| 161 |
-
return standings_string
|
| 162 |
-
|
| 163 |
-
def track_visualization(year: int, round: gp, visualization_type: str, driver_name: str) -> Image.Image:
|
| 164 |
-
"""Generate a visualization of the track with specified data.
|
| 165 |
-
|
| 166 |
-
Args:
|
| 167 |
-
year (int): The season year
|
| 168 |
-
round (str | int): The race round number or name
|
| 169 |
-
visualization_type (str): Type of visualization ('speed', 'corners', or 'gear')
|
| 170 |
-
driver_name (str): Name of the driver for driver-specific visualizations
|
| 171 |
-
|
| 172 |
-
Returns:
|
| 173 |
-
Image.Image: A PIL Image object containing the visualization
|
| 174 |
-
"""
|
| 175 |
-
|
| 176 |
-
session = get_session(year, round, "race")
|
| 177 |
-
session.load()
|
| 178 |
-
|
| 179 |
-
if visualization_type == "speed":
|
| 180 |
-
return track_utils.create_track_speed_visualization(session, driver_name)
|
| 181 |
-
elif visualization_type == "corners":
|
| 182 |
-
return track_utils.create_track_corners_visualization(session)
|
| 183 |
-
elif visualization_type == "gear":
|
| 184 |
-
return track_utils.create_track_gear_visualization(session)
|
| 185 |
-
|
| 186 |
-
def get_session_results(year: int, round: gp, session_type: session_type) -> pd.DataFrame:
|
| 187 |
-
"""Retrieve and format the results of a specific session.
|
| 188 |
-
|
| 189 |
-
Args:
|
| 190 |
-
year (int): The season year
|
| 191 |
-
round (str | int): The race round number or name
|
| 192 |
-
session_type (str | int | None): Type of session (e.g., 'Q', 'R', 'Sprint')
|
| 193 |
-
|
| 194 |
-
Returns:
|
| 195 |
-
pd.DataFrame: DataFrame containing the session results
|
| 196 |
-
|
| 197 |
-
Raises:
|
| 198 |
-
gr.Error: If the session type is not supported for the specified round
|
| 199 |
-
"""
|
| 200 |
-
|
| 201 |
-
try:
|
| 202 |
-
session = get_session(year, round, session_type)
|
| 203 |
-
session.load(telemetry=False)
|
| 204 |
-
results = session.results
|
| 205 |
-
except ValueError as e:
|
| 206 |
-
raise gr.Error(f"Session type {session_type} is not supported for the specified round. This Grand Prix most likely did not include a sprint race/quali.")
|
| 207 |
-
|
| 208 |
-
df = results[["DriverNumber", "Abbreviation", "FullName", "Position", "GridPosition", "Points", "Status", "Q1", "Q2", "Q3"]]
|
| 209 |
-
df["Name"] = df.apply(lambda row: f"{row['FullName']} ({row['Abbreviation']} • {row['DriverNumber']})", axis=1)
|
| 210 |
-
df = df.drop(columns=["FullName", "Abbreviation", "DriverNumber"])
|
| 211 |
-
df = df.rename(columns={"Position": "Pos", "GridPosition": "Grid Pos"})
|
| 212 |
-
|
| 213 |
-
# Process results based on session type
|
| 214 |
-
if session_type in ["race", "sprint"]:
|
| 215 |
-
df = df[["Pos", "Name", "Points", "Grid Pos", "Status"]]
|
| 216 |
-
elif "qualifying" in session_type:
|
| 217 |
-
df[["Q1", "Q2", "Q3"]] = df[["Q1", "Q2", "Q3"]].apply(lambda x: x.dt.total_seconds().apply(lambda y: f"{int(y//60):02d}:{int(y%60):02d}.{int(y%1*1000):03d}" if pd.notna(y) else "-"))
|
| 218 |
-
df = df[["Pos", "Name", "Q1", "Q2", "Q3"]]
|
| 219 |
-
return df
|
| 220 |
-
|
| 221 |
-
def get_driver_info(driver_name: str) -> str:
|
| 222 |
-
"""Retrieve detailed information about a specific driver.
|
| 223 |
-
|
| 224 |
-
Args:
|
| 225 |
-
driver_name (str): Full name of the driver (e.g., 'Max Verstappen')
|
| 226 |
-
|
| 227 |
-
Returns:
|
| 228 |
-
str: Formatted string with driver's details including name, team, number,
|
| 229 |
-
nationality, and a brief summary
|
| 230 |
-
"""
|
| 231 |
-
driver = DRIVER_DETAILS[driver_name]
|
| 232 |
-
driver_info_string = f"{driver_name} ({driver['birth_date']})\n{driver['team']} #{driver['number']}\n{driver['nationality']}\n\n{driver['summary']}"
|
| 233 |
-
return driver_info_string
|
| 234 |
-
|
| 235 |
-
### OpenF1 tools ###
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
if __name__ == "__main__":
|
| 242 |
-
session = get_session(2024, 1, "fp1")
|
| 243 |
-
session.load()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
utils/request_utils.py
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
""" Utils file for handling OpenF1 requests """
|
|
|
|
|
|