import ast
import csv
from typing import Iterator, List
import requests
from .compsim import Competition
from .compsim.models import CompetitionScoring
from .track import CornerListInput, from_corners
from .vehicle import Car
from .vehicle.models import VehicleModel
from .yaml import _competition_from_car_and_comp_dict
def _fetch_sheet_csv(url: str) -> Iterator[List[str]]:
"""
Fetch CSV data from a published Google Sheets URL.
Parameters
----------
url : str
Published Google Sheets URL in CSV export format.
Example: "https://docs.google.com/spreadsheets/d/e/SHEET_ID/pub?output=csv"
Returns
-------
Iterator[List[str]]
CSV reader object containing the sheet data.
Raises
------
RuntimeError
If the HTTP request fails or times out.
"""
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
decoded_content = response.content.decode("utf-8")
return csv.reader(decoded_content.splitlines())
except requests.RequestException as e:
raise RuntimeError(f"Failed to fetch sheet data from {url}: {e}")
def _convert_value(value: str):
"""
Convert string value to appropriate Python type.
Parameters
----------
value : str
String value from CSV.
Returns
-------
int, float, bool, list, or str
Converted value with appropriate type.
"""
if isinstance(value, str):
# First try to parse as a Python literal (handles lists, tuples, etc.)
value = value.strip()
# Then try numeric/boolean conversions
try:
if value.startswith(("[", "(", "{")):
return ast.literal_eval(value)
if "." in value:
return float(value)
elif value.lower() == "true":
return True
elif value.lower() == "false":
return False
else:
return int(value)
except ValueError:
pass # Keep as string if conversion fails
return value
def _parse_sheet_data(csv_obj: Iterator[List[str]]) -> List[List[str]]:
"""
Parse CSV data into [path, value] pairs.
Parameters
----------
csv_obj : Iterator[List[str]]
CSV reader object from Google Sheets. Expects at least 4 columns:
[Parameter Path, Description, Units, Value1, Value2, ...]
Returns
-------
List[List[str]]
List of [parameter_path, value, units, description] pairs.
Example:
[
["mass", "258", "kg", "Total mass of the vehicle"],
["aero/cd", "1.57", "unitless", "Drag coefficient"]
]
Raises
------
ValueError
If the CSV is empty or has invalid structure.
"""
result = []
try:
for row in csv_obj:
if not row or row[0] == "":
continue
# Handle multiple values per parameter
if len(row) > 4 and row[4] != "":
val = []
i = 3
while i < len(row) and row[i] != "":
val.append(_convert_value(row[i]))
i += 1
else:
val = _convert_value(row[3])
result.append([row[0], val, row[1], row[2]])
except IndexError:
raise ValueError(
"Invalid CSV structure. Expected at least 4 columns, "
"[Parameter Path, Description, Units, Value[0], ...]"
)
return result
def _build_dict(values: List[List[str]]) -> dict:
"""
Parse values and built a dictionary
Parameters
----------
values : List[List[str]]
List of [path, value] pairs to override.
Paths use "/" to separate nested keys (e.g., "aero/cd").
Returns
-------
dict
Nested dictionary with parsed values.
"""
result = dict()
for v in values[1:]: # Skip header row
# Split path and traverse the dictionary
keys = v[0].split("/")
current = result
# Navigate to parent of target key
for key in keys[:-1]:
if key not in current:
current[key] = dict()
current = current[key]
# Safety check: don't overwrite non-leaf nodes
if keys[-1] in current:
if isinstance(current[keys[-1]], dict):
raise ValueError(
f"Parameter '{v[0]}' is malformed. "
f"Collides with existing parameters"
)
current[keys[-1]] = v[1]
return result
[docs]
def load_car_from_gdrive(url: str) -> Car:
"""
Load a Car object from a published Google Sheets CSV export.
Parameters
----------
url : str
Link to a Google Sheets CSV export specifying vehicle configuration.
Returns
-------
Car
Notes
-------
URLs must be in the form: "https://docs.google.com/spreadsheets/d/e/SHEET_ID/pub?output=csv"
To get this URL:
1. In Google Sheets, go to File > Share > Publish to web
2. Select the desired sheet and CSV format
3. Copy the generated URL
"""
csv_data = _fetch_sheet_csv(url)
parsed_data = _parse_sheet_data(csv_data)
nested_dict = _build_dict(parsed_data)
vehicle_model = VehicleModel.model_validate(nested_dict)
return Car(vehicle_model=vehicle_model)
[docs]
def load_competition_from_gdrive(
comp_config_url: str,
car_config_url: str,
) -> Competition:
"""
Create a Competition object from a published Google Sheets CSV export.
This function fetches competition configuration data (scoring rules, track
definitions, etc.) from a published Google Sheet and returns a configured
Competition object ready for event simulation.
Parameters
----------
car_config_url : str
Link to a Google Sheets CSV export specifying vehicle configuration.
comp_config_url : str
Link to a Google Sheets CSV export specifying competition configuration.
Returns
-------
Competition
Notes
-------
URLs must be in the form: "https://docs.google.com/spreadsheets/d/e/SHEET_ID/pub?output=csv"
To get this URL:
1. In Google Sheets, go to File > Share > Publish to web
2. Select the desired sheet and CSV format
3. Copy the generated URL
"""
car = load_car_from_gdrive(car_config_url)
comp_csv_data = _fetch_sheet_csv(comp_config_url)
comp_parsed_data = _parse_sheet_data(comp_csv_data)
comp_config = _build_dict(comp_parsed_data)
return _competition_from_car_and_comp_dict(car, comp_config)