Source code for suboptimumg.gdrive

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)