diff --git a/CHANGELOG.md b/CHANGELOG.md index c65ec36..a21f8eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +# v0.4.0 + +* Added several tests +* Added TPMS status (`vehicle.tires.system_status`) +* Added GPS coordinates (`vehicle.position`) +* Added oil life remaining (`vehicle.oil_life_remaining`) +* Added seatbelt status (`vehicle.seatbelts`) +* Refactored tires to include TPMS status for each tire (`vehicle.tires.front_right.status`) + +# v0.3.0 + +* Added `Pressure.bar` unit +* Fix URLs in pyproject.toml +* Fix door logic + # v0.3.0 * Added `Pressure.bar` unit diff --git a/README.md b/README.md index bddc9c0..c9a27e2 100644 --- a/README.md +++ b/README.md @@ -84,3 +84,7 @@ Contributions welcome! Please [open an issue](https://github.com/austind/vehicle nox -r # The -r flag re-uses virtualenvs for faster re-runs ``` 1. [Open a pull request](https://github.com/austind/vehiclepass/compare) + +## Credits + +Based on [IOS shortcuts](https://www.reddit.com/r/f150/comments/s6cs3r/siri_shortcuts_for_fordpass_fixed/) originally developed by [u/d4v3y0rk](https://reddit.com/u/d4v3y0rk) and [/u/tinybtg](https://reddit.com/u/tinybtg), and currently maintained by [Brandon Grant](https://brandongrant.dev/). diff --git a/pyproject.toml b/pyproject.toml index ce589b5..903dccf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,14 +3,13 @@ name = "vehiclepass" version = "0.3.0" description = "A Python API to manage your FordPass-enabled vehicle." readme = "README.md" -authors = [ - { name = "Austin de Coup-Crank", email = "austindcc@gmail.com" } -] +authors = [{ name = "Austin de Coup-Crank", email = "austindcc@gmail.com" }] requires-python = ">=3.9" dependencies = [ "httpx>=0.28.1", - "pydantic>=2.11.1", - "python-dotenv>=1.1.0", + "pydantic>=2.11.1", + "pydantic-extra-types>=2.10.3", + "python-dotenv>=1.1.0", ] classifiers = [ @@ -70,13 +69,7 @@ select = [ "D", # pydocstyle ] -exclude = [ - ".git", - ".venv", - "__pycache__", - "build", - "dist", -] +exclude = [".git", ".venv", "__pycache__", "build", "dist"] [tool.ruff.lint.isort] known-first-party = ["vehiclepass"] @@ -84,3 +77,7 @@ combine-as-imports = true [tool.ruff.lint.pydocstyle] convention = "google" + +[tool.ruff.lint.per-file-ignores] +"tests/*" = ["B009"] +"./hack.py" = ["ALL"] diff --git a/src/vehiclepass/_types.py b/src/vehiclepass/_types.py index b111626..5734228 100644 --- a/src/vehiclepass/_types.py +++ b/src/vehiclepass/_types.py @@ -5,3 +5,4 @@ VehicleCommand = Literal["remoteStart", "cancelRemoteStart", "lock", "unlock"] AlarmStatus = Literal["ARMED", "DISARMED"] GearLeverPosition = Literal["PARK", "REVERSE", "NEUTRAL", "DRIVE", "MANUAL"] +TirePressureStatus = Literal["NORMAL", "LOW", "HIGH"] diff --git a/src/vehiclepass/constants.py b/src/vehiclepass/constants.py index 26a9d0a..6335661 100644 --- a/src/vehiclepass/constants.py +++ b/src/vehiclepass/constants.py @@ -1,6 +1,7 @@ """Constants.""" import os +from pathlib import Path from dotenv import load_dotenv @@ -51,3 +52,5 @@ f"VEHICLEPASS_DEFAULT_TIME_UNIT: Invalid unit: {DEFAULT_TIME_UNIT}. " "Valid units are: s, m, h, ms, human_readable" ) + +MOCK_RESPONSES_DIR = Path(__file__).parent.parent.parent / "tests" / "fixtures" / "responses" diff --git a/src/vehiclepass/doors.py b/src/vehiclepass/doors.py index a47ef15..76f8644 100644 --- a/src/vehiclepass/doors.py +++ b/src/vehiclepass/doors.py @@ -21,7 +21,7 @@ def __init__(self, vehicle: "Vehicle") -> None: vehicle: Parent vehicle object Raises: - StatusError: If status_data is None or empty + None """ self._vehicle = vehicle self._doors = {} diff --git a/src/vehiclepass/seatbelts.py b/src/vehiclepass/seatbelts.py new file mode 100644 index 0000000..68a3f5a --- /dev/null +++ b/src/vehiclepass/seatbelts.py @@ -0,0 +1,36 @@ +"""Seatbelt status.""" + +import logging +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from vehiclepass.vehicle import Vehicle + +logger = logging.getLogger(__name__) + + +class SeatBelts: + """Seatbelt status.""" + + def __init__(self, vehicle: "Vehicle"): + """Initialize seatbelt status readings from status data and dynamically create properties. + + Args: + vehicle: Parent vehicle object + + Raises: + None + """ + self._vehicle = vehicle + self._seatbelts = {} + self._seatbelt_status = self._vehicle._get_metric_value("seatBeltStatus", list) + + for seatbelt in self._seatbelt_status: + position = seatbelt.get("vehicleOccupantRole", "").lower() + self._seatbelts[position] = seatbelt["value"] + setattr(self, position, seatbelt["value"]) + + def __repr__(self) -> str: + """Return string representation showing available seatbelts.""" + positions = list(self._seatbelts.keys()) + return f"SeatBelts(seats={positions})" diff --git a/src/vehiclepass/tire_pressure.py b/src/vehiclepass/tire_pressure.py deleted file mode 100644 index e9c006f..0000000 --- a/src/vehiclepass/tire_pressure.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Tire pressure reading for all vehicle wheels.""" - -from typing import TYPE_CHECKING - -from vehiclepass.units import Pressure - -if TYPE_CHECKING: - from vehiclepass.vehicle import Vehicle - - -class TirePressure: - """Represents tire pressure readings for all vehicle wheels.""" - - def __init__(self, vehicle: "Vehicle") -> None: - """Initialize tire pressure readings from status data and dynamically create properties. - - Args: - vehicle: The parent Vehicle object - """ - self._vehicle = vehicle - self._tires = {} - for tire in self._vehicle._get_metric_value("tirePressure", list): - tire_position = tire["vehicleWheel"].lower() - self._tires[tire_position] = Pressure.from_kilopascals(tire["value"]) - setattr(self, tire_position, self._tires[tire_position]) - - def __repr__(self): - """Return string representation showing available tire positions.""" - positions = list(self._tires.keys()) - return f"TirePressure(positions={positions})" diff --git a/src/vehiclepass/tires.py b/src/vehiclepass/tires.py new file mode 100644 index 0000000..2d71568 --- /dev/null +++ b/src/vehiclepass/tires.py @@ -0,0 +1,83 @@ +"""Tire pressure reading for all vehicle tires.""" + +import logging +from typing import TYPE_CHECKING + +from vehiclepass._types import TirePressureStatus +from vehiclepass.errors import StatusError +from vehiclepass.units import Pressure + +if TYPE_CHECKING: + from vehiclepass.vehicle import Vehicle + +logger = logging.getLogger(__name__) + + +class Tire: + """Represents tire status for a single tire.""" + + def __init__(self, vehicle: "Vehicle", tire_position: str, pressure: Pressure, status: TirePressureStatus) -> None: + """Initialize tire status for a single tire.""" + self._vehicle = vehicle + self._tire_position = tire_position + self.pressure = pressure + self.status = status + + def __repr__(self): + """Return string representation showing tire position and pressure.""" + return f"Tire(position={self._tire_position}, pressure={self.pressure})" + + +class Tires: + """Represents tire status for all vehicle tires.""" + + def __init__(self, vehicle: "Vehicle") -> None: + """Initialize tire status from status data and dynamically create properties. + + Args: + vehicle: The parent Vehicle object + """ + self._vehicle = vehicle + self._tires = {} + for tire in self._vehicle._get_metric_value("tirePressure", list): + tire_position = tire["vehicleWheel"].lower() + logger.debug("Tire position: %s", tire_position) + self._tires[tire_position] = {"pressure": Pressure.from_kilopascals(tire["value"])} + + for tire in self._vehicle._get_metric_value("tirePressureStatus", list): + tire_position = tire["vehicleWheel"].lower() + self._tires[tire_position].update({"status": tire["value"]}) + + for tire_position, data in self._tires.items(): + setattr( + self, + tire_position, + Tire( + self._vehicle, + tire_position, + pressure=data["pressure"], + # Seems to trigger a mypy bug + status=data["status"], # type: ignore + ), + ) + + @property + def system_status(self) -> str: + """Get the status of the tire pressure monitoring system.""" + try: + return self._vehicle._get_metric_value("tirePressureSystemStatus", list)[0]["value"] + except (IndexError, KeyError) as exc: + raise StatusError("Tire pressure monitoring system status not found") from exc + + @property + def data(self) -> dict: + """Get the pressure and status data for all tires.""" + return { + tire_position: {"pressure": tire["pressure"].data, "status": tire["status"]} + for tire_position, tire in self._tires.items() + } + + def __repr__(self): + """Return string representation showing available tire positions.""" + positions, values = zip(*self._tires.items()) + return f"Tires(positions={positions}, values={values})" diff --git a/src/vehiclepass/units.py b/src/vehiclepass/units.py index 2ce0967..7c29640 100644 --- a/src/vehiclepass/units.py +++ b/src/vehiclepass/units.py @@ -1,8 +1,10 @@ -"""Unit conversion utilities.""" +"""Unit conversion utilities using Pydantic models.""" import datetime -from dataclasses import dataclass, field -from typing import Literal, TypeVar, Union +from typing import Annotated, Literal, Optional, TypeVar + +from pydantic import BaseModel, Field, NonNegativeInt +from pydantic.functional_validators import AfterValidator from vehiclepass.constants import ( DECIMAL_PLACES, @@ -18,8 +20,19 @@ TemperatureUnit = Literal["c", "f"] DistanceUnit = Literal["km", "mi"] PressureUnit = Literal["kpa", "psi"] - -unit_label_map = { +ElectricPotentialUnit = Literal["v", "mv"] +TimeUnit = Literal["h", "m", "s", "ms", "human_readable"] + +# Type aliases for better type checking +Celsius = Annotated[float, AfterValidator(lambda x: round(x, DECIMAL_PLACES))] +Kilometers = Annotated[float, AfterValidator(lambda x: round(x, DECIMAL_PLACES))] +Kilopascals = Annotated[float, AfterValidator(lambda x: round(x, DECIMAL_PLACES))] +Volts = Annotated[float, AfterValidator(lambda x: round(x, DECIMAL_PLACES))] +Seconds = Annotated[float, AfterValidator(lambda x: round(x, DECIMAL_PLACES))] +PercentValue = Annotated[float, AfterValidator(lambda x: round(x, DECIMAL_PLACES))] + +# Unit label mapping for string representations +UNIT_LABEL_MAP: dict[str, str] = { "c": "°C", "f": "°F", "km": "km", @@ -34,182 +47,203 @@ } -@dataclass(frozen=True) -class Temperature: +class UnitModel(BaseModel): + """Base model for unit conversion models.""" + + decimal_places: Optional[int] = Field(default=DECIMAL_PLACES, exclude=True) + _units: tuple[str, ...] = () + model_config = { + "frozen": True, + } + + def round_value(self, value: float, decimal_places: Optional[NonNegativeInt] = None) -> float: + """Round a value to the specified decimal places.""" + return round(value, decimal_places or self.decimal_places or DECIMAL_PLACES) + + @property + def data(self) -> dict[str, float]: + """Get all data units in a dictionary.""" + return {unit: getattr(self, unit) for unit in self._units} + + +class Temperature(UnitModel): """Temperature value with unit conversion capabilities.""" - celsius: float - _decimal_places: int = field(default=DECIMAL_PLACES) + celsius: Celsius + _units = ("c", "f") @property def c(self) -> float: """Get temperature in Celsius.""" - return round(self.celsius, self._decimal_places) + return self.round_value(self.celsius) @property def f(self) -> float: """Get temperature in Fahrenheit.""" - return round((self.celsius * 9 / 5) + 32, self._decimal_places) + return self.round_value((self.celsius * 9 / 5) + 32) @classmethod - def from_celsius(cls, value: float, decimal_places: int = DECIMAL_PLACES) -> "Temperature": + def from_celsius(cls, value: float, decimal_places: Optional[int] = None) -> "Temperature": """Create a Temperature instance from a Celsius value.""" - return cls(value, decimal_places) + return cls(celsius=value, decimal_places=decimal_places) @classmethod - def from_fahrenheit(cls, value: float, decimal_places: int = DECIMAL_PLACES) -> "Temperature": + def from_fahrenheit(cls, value: float, decimal_places: Optional[int] = None) -> "Temperature": """Create a Temperature instance from a Fahrenheit value.""" - return cls((value - 32) * 5 / 9, decimal_places) + return cls(celsius=(value - 32) * 5 / 9, decimal_places=decimal_places) def __str__(self) -> str: """Return a string representation of the temperature.""" - return f"{getattr(self, DEFAULT_TEMP_UNIT)}{unit_label_map[DEFAULT_TEMP_UNIT]}" + value = getattr(self, DEFAULT_TEMP_UNIT) + unit = UNIT_LABEL_MAP[DEFAULT_TEMP_UNIT] + return f"{value}{unit}" -@dataclass(frozen=True) -class Distance: +class Distance(UnitModel): """Distance value with unit conversion capabilities.""" - kilometers: float - _decimal_places: int = field(default=DECIMAL_PLACES) + kilometers: Kilometers + _units = ("km", "mi") @property def km(self) -> float: """Get distance in kilometers.""" - return round(self.kilometers, self._decimal_places) + return self.round_value(self.kilometers) @property def mi(self) -> float: """Get distance in miles.""" - return round(self.kilometers * 0.621371, self._decimal_places) + return self.round_value(self.kilometers * 0.621371) @classmethod - def from_kilometers(cls, value: float, decimal_places: int = DECIMAL_PLACES) -> "Distance": + def from_kilometers(cls, value: float, decimal_places: Optional[int] = None) -> "Distance": """Create a Distance instance from a kilometers value.""" - return cls(value, decimal_places) + return cls(kilometers=value, decimal_places=decimal_places) @classmethod - def from_miles(cls, value: float, decimal_places: int = DECIMAL_PLACES) -> "Distance": + def from_miles(cls, value: float, decimal_places: Optional[int] = None) -> "Distance": """Create a Distance instance from a miles value.""" - return cls(value / 0.621371, decimal_places) + return cls(kilometers=value / 0.621371, decimal_places=decimal_places) def __str__(self) -> str: """Return a string representation of the distance.""" - return f"{getattr(self, DEFAULT_DISTANCE_UNIT)} {unit_label_map[DEFAULT_DISTANCE_UNIT]}" + value = getattr(self, DEFAULT_DISTANCE_UNIT) + unit = UNIT_LABEL_MAP[DEFAULT_DISTANCE_UNIT] + return f"{value} {unit}" -@dataclass(frozen=True) -class Pressure: +class Pressure(UnitModel): """Pressure value with unit conversion capabilities.""" - kilopascals: float - _decimal_places: int = field(default=DECIMAL_PLACES) + kilopascals: Kilopascals + _units = ("kpa", "psi", "bar") @property def bar(self) -> float: """Get pressure in bars.""" - return round(self.kilopascals / 100, self._decimal_places) + return self.round_value(self.kilopascals / 100) @property def kpa(self) -> float: """Get pressure in kilopascals.""" - return round(self.kilopascals, self._decimal_places) + return self.round_value(self.kilopascals) @property def psi(self) -> float: """Get pressure in pounds per square inch.""" - return round(self.kilopascals * 0.145038, self._decimal_places) + return self.round_value(self.kilopascals * 0.145038) @classmethod - def from_kilopascals(cls, value: float, decimal_places: int = DECIMAL_PLACES) -> "Pressure": + def from_kilopascals(cls, value: float, decimal_places: Optional[int] = None) -> "Pressure": """Create a Pressure instance from a kilopascals value.""" - return cls(value, decimal_places) + return cls(kilopascals=value, decimal_places=decimal_places) @classmethod - def from_psi(cls, value: float, decimal_places: int = DECIMAL_PLACES) -> "Pressure": + def from_psi(cls, value: float, decimal_places: Optional[int] = None) -> "Pressure": """Create a Pressure instance from a psi value.""" - return cls(value / 0.145038, decimal_places) + return cls(kilopascals=value / 0.145038, decimal_places=decimal_places) def __str__(self) -> str: """Return a string representation of the pressure.""" - return f"{getattr(self, DEFAULT_PRESSURE_UNIT)} {unit_label_map[DEFAULT_PRESSURE_UNIT]}" + value = getattr(self, DEFAULT_PRESSURE_UNIT) + unit = UNIT_LABEL_MAP[DEFAULT_PRESSURE_UNIT] + return f"{value} {unit}" -@dataclass(frozen=True) -class ElectricPotential: +class ElectricPotential(UnitModel): """Electric potential value with unit conversion capabilities.""" - volts: Union[float, int] - _decimal_places: int = field(default=DECIMAL_PLACES) + volts: Volts + _units = ("v", "mv") @property def v(self) -> float: """Get electric potential in volts.""" - return round(self.volts, self._decimal_places) + return self.round_value(self.volts) @property def mv(self) -> float: """Get electric potential in millivolts.""" - return round(self.volts * 1000, self._decimal_places) + return self.round_value(self.volts * 1000) @classmethod - def from_volts(cls, value: float, decimal_places: int = DECIMAL_PLACES) -> "ElectricPotential": + def from_volts(cls, value: float, decimal_places: Optional[int] = None) -> "ElectricPotential": """Create an ElectricPotential instance from a volts value.""" - return cls(value, decimal_places) + return cls(volts=value, decimal_places=decimal_places) @classmethod - def from_millivolts(cls, value: float, decimal_places: int = DECIMAL_PLACES) -> "ElectricPotential": + def from_millivolts(cls, value: float, decimal_places: Optional[int] = None) -> "ElectricPotential": """Create an ElectricPotential instance from a millivolts value.""" - return cls(value / 1000, decimal_places) + return cls(volts=value / 1000, decimal_places=decimal_places) def __str__(self) -> str: """Return a string representation of the electric potential.""" - return f"{getattr(self, DEFAULT_ELECTRIC_POTENTIAL_UNIT)} {unit_label_map[DEFAULT_ELECTRIC_POTENTIAL_UNIT]}" + value = getattr(self, DEFAULT_ELECTRIC_POTENTIAL_UNIT) + unit = UNIT_LABEL_MAP[DEFAULT_ELECTRIC_POTENTIAL_UNIT] + return f"{value} {unit}" -@dataclass(frozen=True) -class Percentage: +class Percentage(UnitModel): """Percentage value.""" - percentage: float - _decimal_places: int = field(default=DECIMAL_PLACES) + value: float = Field(alias="percentage") @property def percent(self) -> float: - """Get percentage.""" - return round(self.percentage, self._decimal_places) + """Get the rounded percentage value.""" + # Since percents are already decimals, we need to add 2 more decimal places to the value + return self.round_value(self.value, decimal_places=self.decimal_places + 2 if self.decimal_places else None) def __str__(self) -> str: """Return a string representation of the percentage.""" return f"{self.percent * 100}%" -@dataclass(frozen=True) -class Duration: +class Duration(UnitModel): """Time duration.""" - seconds: float - _decimal_places: int = field(default=DECIMAL_PLACES) + seconds: Seconds + _units = ("h", "m", "s", "ms", "human_readable") @property def h(self) -> float: """Get duration in hours.""" - return round(self.seconds / 3600, self._decimal_places) + return self.round_value(self.seconds / 3600) @property def m(self) -> float: """Get duration in minutes.""" - return round(self.seconds / 60, self._decimal_places) + return self.round_value(self.seconds / 60) @property def s(self) -> float: """Get duration in seconds.""" - return round(self.seconds, self._decimal_places) + return self.round_value(self.seconds) @property def ms(self) -> float: """Get duration in milliseconds.""" - return round(self.seconds * 1000, self._decimal_places) + return self.round_value(self.seconds * 1000) @property def delta(self) -> datetime.timedelta: @@ -221,21 +255,28 @@ def human_readable(self) -> str: """Get duration in human readable format.""" parts = [] if self.h >= 1: - parts.append(f"{round(self.h)}h") - if self.m > 0: - parts.append(f"{round(self.m)}m") - remaining_seconds = self.seconds % 60 # Calculate remaining seconds - if remaining_seconds > 0: - parts.append(f"{round(remaining_seconds)}s") - return " ".join(parts) if parts else "0s" + parts.append(f"{int(self.h)}h") + + minutes = int(self.m) % 60 + if minutes > 0: + parts.append(f"{minutes}m") + + remaining_seconds = int(self.seconds) % 60 + if remaining_seconds > 0 or not parts: + parts.append(f"{remaining_seconds}s") + + return " ".join(parts) @classmethod - def from_seconds(cls, value: float, decimal_places: int = DECIMAL_PLACES) -> "Duration": - """Create a Time instance from a seconds value.""" - return cls(value, decimal_places) + def from_seconds(cls, value: float, decimal_places: Optional[int] = None) -> "Duration": + """Create a Duration instance from a seconds value.""" + return cls(seconds=value, decimal_places=decimal_places) def __str__(self) -> str: """Return a string representation of the time.""" if DEFAULT_TIME_UNIT == "human_readable": return self.human_readable - return f"{getattr(self, DEFAULT_TIME_UNIT)} {unit_label_map[DEFAULT_TIME_UNIT]}" + + value = getattr(self, DEFAULT_TIME_UNIT) + unit = UNIT_LABEL_MAP[DEFAULT_TIME_UNIT] + return f"{value} {unit}" diff --git a/src/vehiclepass/vehicle.py b/src/vehiclepass/vehicle.py index e84499b..d448748 100644 --- a/src/vehiclepass/vehicle.py +++ b/src/vehiclepass/vehicle.py @@ -11,6 +11,7 @@ import httpx from dotenv import load_dotenv from pydantic import NonNegativeInt +from pydantic_extra_types.coordinate import Coordinate, Latitude, Longitude from vehiclepass._types import AlarmStatus, CompassDirection, GearLeverPosition, HoodStatus, VehicleCommand from vehiclepass.constants import ( @@ -25,7 +26,8 @@ from vehiclepass.doors import Doors from vehiclepass.errors import CommandError, StatusError from vehiclepass.indicators import Indicators -from vehiclepass.tire_pressure import TirePressure +from vehiclepass.seatbelts import SeatBelts +from vehiclepass.tires import Tires from vehiclepass.units import Distance, Duration, ElectricPotential, Percentage, Temperature load_dotenv() @@ -106,7 +108,7 @@ def _get_metric_value(self, metric_name: str, expected_type: Optional[type[T]] = expected_type: The expected type of the value (optional) Returns: - The metric value, rounded to 2 decimal places if numeric + The metric value Raises: StatusError: If the metric is not found or invalid @@ -124,7 +126,7 @@ def _get_metric_value(self, metric_name: str, expected_type: Optional[type[T]] = value = metric if expected_type is not None and not isinstance(value, expected_type): - raise StatusError(f"Invalid {metric_name} type") + raise StatusError(f"Invalid {metric_name} type: expected {expected_type}, got {type(value)}") return value # type: ignore except Exception as exc: if isinstance(exc, StatusError): @@ -400,7 +402,7 @@ def alarm_status(self) -> AlarmStatus: @property def battery_charge(self) -> Percentage: """Get the battery charge percentage.""" - return Percentage(self._get_metric_value("batteryStateOfCharge", float) / 100) + return Percentage(percentage=self._get_metric_value("batteryStateOfCharge", float) / 100) @property def battery_voltage(self) -> ElectricPotential: @@ -425,7 +427,7 @@ def engine_coolant_temp(self) -> Temperature: @property def fuel_level(self) -> Percentage: """Get the fuel level as a percentage.""" - return Percentage(self._get_metric_value("fuelLevel", float) / 100) + return Percentage(percentage=self._get_metric_value("fuelLevel", float) / 100) @property def fuel_range(self) -> Distance: @@ -485,11 +487,21 @@ def is_running(self) -> bool: """Check if the vehicle is running, either from the ignition or a remote start command.""" return self.is_ignition_started or self.is_remotely_started + @property + def location(self): + """Get detailed location data.""" + raise NotImplementedError("location not yet implemented") + @property def odometer(self) -> Distance: """Get the odometer reading.""" return Distance.from_kilometers(self._get_metric_value("odometer", float)) + @property + def oil_life_remaining(self) -> Percentage: + """Get oil life remaining as a percentage.""" + return Percentage(percentage=self._get_metric_value("oilLifeRemaining", float) / 100) + @property def outside_temp(self) -> Temperature: """Get the outside temperature using the configured unit preferences. @@ -499,11 +511,28 @@ def outside_temp(self) -> Temperature: """ return Temperature.from_celsius(self._get_metric_value("outsideTemperature", float)) + @property + def position(self) -> Coordinate: + """Get vehicle location (latitude/longitude).""" + position = self._get_metric_value("position", dict) + try: + return Coordinate( + latitude=Latitude(round(position["location"]["lat"], 5)), + longitude=Longitude(round(position["location"]["lon"], 5)), + ) + except KeyError as exc: + raise StatusError("Unable to find vehicle position.") from exc + @property def rpm(self) -> NonNegativeInt: """Get the engine's current RPM.""" return self._get_metric_value("engineSpeed", int) + @property + def seatbelts(self) -> SeatBelts: + """Get seatbelt status.""" + return SeatBelts(self) + @property def shutoff_countdown(self) -> Duration: """Get the vehicle shutoff time in seconds.""" @@ -524,6 +553,16 @@ def status(self) -> dict: return self._status @property - def tire_pressure(self) -> TirePressure: - """Get the tire pressure readings.""" - return TirePressure(self) + def tires(self) -> Tires: + """Get tires.""" + return Tires(self) + + @property + def tpms(self) -> Tires: + """Get tire pressure monitoring system.""" + return self.tires + + @property + def tyres(self) -> Tires: + """Get tyres.""" + return self.tires diff --git a/tests/conftest.py b/tests/conftest.py index 7b0d2ad..7535fec 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,25 +1,12 @@ """Fixtures for testing the vehiclepass library using respx.""" -import functools -import json import logging -import re -from collections.abc import Callable, Iterable -from itertools import chain, repeat from pathlib import Path -from typing import Any, TypeVar, Union +from typing import TypeVar -import httpx import pytest import respx -from vehiclepass.constants import ( - AUTONOMIC_AUTH_URL, - AUTONOMIC_COMMAND_BASE_URL, - AUTONOMIC_TELEMETRY_BASE_URL, - FORDPASS_AUTH_URL, -) - T = TypeVar("T") MOCK_RESPONSES_DIR = Path(__file__).parent / "fixtures" / "responses" @@ -30,151 +17,6 @@ def pytest_configure(config): logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)") -def load_mock_json(file_path: Union[str, Path]) -> dict[str, Any]: - """Load mock data from a JSON file. - - Args: - file_path: Path to the JSON file - - Returns: - Dict containing the loaded JSON data - """ - original_path = Path(file_path) - final_path = original_path - - # Check if it's a relative path and try to find it in mock_data directory - if not original_path.is_absolute(): - if original_path.exists(): - final_path = original_path - else: - relative_path = MOCK_RESPONSES_DIR / original_path - if relative_path.exists(): - final_path = relative_path - else: - raise FileNotFoundError(f"Mock data file not found: {original_path}") - - with open(final_path) as f: - return json.load(f) - - -def mock_responses( - status: Union[httpx.Response, str, Path, list[Union[str, Path, httpx.Response]], dict[str, Any], None] = None, - commands: Union[ - dict[str, Union[httpx.Response, str, Path, list[Union[str, Path, httpx.Response]], dict[str, Any]]], None - ] = None, - auth_token: str = "mock-token-12345", -): - """Test decorator that mocks HTTP API responses. - - Intercepts network requests for the FordPass API and returns predefined mock - responses instead of making actual API calls. - - Args: - status: Controls responses for status/telemetry endpoint requests. - - httpx.Response: Used directly as the response - - str or Path: Path to a JSON file, loaded and returned with 200 status code - - dict: Raw JSON data, returned with 200 status code - - list: Multiple responses returned in sequence for consecutive calls - (last item repeats for any additional calls) - - None: Returns a default baseline status from "status/baseline.json" - - commands: Controls responses for vehicle command endpoint requests. - - dict mapping command names to response definitions - - Each response can be a str/Path to JSON file, raw dict, or a list of - responses returned in sequence (like status) - - None: All command requests return 404 - - auth_token: The mock authentication token to use for all authorized requests. - - Returns: - A decorated test function that runs within the mocked HTTP environment. - - Example: - @mock_responses( - status="tests/mocks/vehicle_status.json", - commands={ - "unlock": "tests/mocks/unlock_success.json", - "lock": [ - "tests/mocks/command_accepted.json", - "tests/mocks/lock_complete.json" - ] - } - ) - def test_vehicle_commands(self): - # Test code that makes API calls, which will now use mock responses - ... - """ - - def decorator(test_func: Callable[..., T]) -> Callable[..., T]: - def get_response_data(source): - if isinstance(source, (str, Path)): - return load_mock_json(source) - return source - - command_indexes = {} - - def get_response(source_data, index_tracker): - # For non-list sources, just return the data directly - if not isinstance(source_data, list): - return get_response_data(source_data) - - # For lists, return the current item or the last one if we've reached the end - current = index_tracker["current"] - if current < len(source_data) - 1: - index_tracker["current"] = current + 1 - return get_response_data(source_data[current]) - # Return the last item for all subsequent calls - return get_response_data(source_data[-1]) - - def status_handler() -> Union[Iterable[httpx.Response], httpx.Response]: - if isinstance(status, (str, Path)): - return httpx.Response(status_code=200, json=load_mock_json(status)) - if isinstance(status, list): - responses = [ - s if isinstance(s, httpx.Response) else httpx.Response(status_code=200, json=load_mock_json(s)) - for s in status - ] - return chain(responses, repeat(responses[-1])) - if isinstance(status, dict): - return httpx.Response(status_code=200, json=status) - return httpx.Response(status_code=200, json=load_mock_json("status/baseline.json")) - - def command_handler(request): - if commands is None: - return httpx.Response(status_code=404) - - command = json.loads(request.content.decode("utf-8")).get("type") - if command not in commands: - return httpx.Response(status_code=404) - - if command not in command_indexes: - command_indexes[command] = {"current": 0} - - response_data = get_response(commands[command], command_indexes[command]) - return ( - response_data - if isinstance(response_data, httpx.Response) - else httpx.Response(status_code=200, json=response_data) - ) - - @functools.wraps(test_func) - def wrapper(*args, **kwargs): - with respx.mock(assert_all_called=False) as mock: - mock.post(FORDPASS_AUTH_URL).respond( - json={"access_token": auth_token, "token_type": "Bearer", "expires_in": 3600} - ) - mock.post(AUTONOMIC_AUTH_URL).respond( - json={"access_token": auth_token, "token_type": "Bearer", "expires_in": 3600} - ) - mock.get(re.compile(rf"{AUTONOMIC_TELEMETRY_BASE_URL}/*")).side_effect = status_handler() # type: ignore - mock.post(re.compile(rf"{AUTONOMIC_COMMAND_BASE_URL}/*")).side_effect = command_handler - return test_func(*args, **kwargs) - - return wrapper - - return decorator - - @pytest.fixture def vehicle(): """Fixture for a basic Vehicle instance.""" diff --git a/tests/fixtures/responses/unauthorized.json b/tests/fixtures/responses/unauthorized.json new file mode 100644 index 0000000..d0bd891 --- /dev/null +++ b/tests/fixtures/responses/unauthorized.json @@ -0,0 +1,7 @@ +{ + "code": 401, + "error": "unauthorized", + "message": "request did not provide a token", + "timestamp": "2025-04-07T11:38:40.735290Z", + "referenceId": "e7f0a5d9-919a-47b6-8509-f2497845f236" +} \ No newline at end of file diff --git a/tests/test_commands.py b/tests/test_commands.py index 952cd09..b9840ad 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -3,8 +3,7 @@ import pytest import vehiclepass - -from .conftest import mock_responses +from tests.utils import mock_responses @mock_responses( diff --git a/tests/test_doors.py b/tests/test_doors.py index c9f1de9..b9ea8ce 100644 --- a/tests/test_doors.py +++ b/tests/test_doors.py @@ -1,10 +1,9 @@ """Test vehicle doors.""" import vehiclepass +from tests.utils import mock_responses from vehiclepass.doors import Doors -from .conftest import mock_responses - @mock_responses(status="status/unlocked.json") def test_doors_closed(vehicle: vehiclepass.Vehicle) -> None: @@ -12,19 +11,14 @@ def test_doors_closed(vehicle: vehiclepass.Vehicle) -> None: assert isinstance(vehicle.doors, Doors) assert vehicle.doors.are_unlocked assert vehicle.doors.are_locked is False - assert hasattr(vehicle.doors, "unspecified_front") - assert hasattr(vehicle.doors, "front_left") - assert hasattr(vehicle.doors, "front_right") - assert hasattr(vehicle.doors, "rear_left") - assert hasattr(vehicle.doors, "rear_right") - assert hasattr(vehicle.doors, "tailgate") - assert hasattr(vehicle.doors, "inner_tailgate") - assert vehicle.doors.front_left == "CLOSED" - assert vehicle.doors.front_right == "CLOSED" - assert vehicle.doors.rear_left == "CLOSED" # type: ignore - assert vehicle.doors.rear_right == "CLOSED" # type: ignore - assert vehicle.doors.tailgate == "CLOSED" # type: ignore - assert vehicle.doors.inner_tailgate == "CLOSED" # type: ignore + # We call these with getattr to avoid mypy errors, as doors are dynamically + # created in the Doors class. + assert getattr(vehicle.doors, "front_left") == "CLOSED" + assert getattr(vehicle.doors, "front_right") == "CLOSED" + assert getattr(vehicle.doors, "rear_left") == "CLOSED" + assert getattr(vehicle.doors, "rear_right") == "CLOSED" + assert getattr(vehicle.doors, "tailgate") == "CLOSED" + assert getattr(vehicle.doors, "inner_tailgate") == "CLOSED" @mock_responses( diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 7db21ac..531590c 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -1,8 +1,7 @@ """Test fixtures.""" import vehiclepass - -from .conftest import mock_responses +from tests.utils import mock_responses @mock_responses( diff --git a/tests/test_start_stop.py b/tests/test_start_stop.py index f4c0659..ba58d89 100644 --- a/tests/test_start_stop.py +++ b/tests/test_start_stop.py @@ -4,8 +4,7 @@ import pytest import vehiclepass - -from .conftest import load_mock_json, mock_responses +from tests.utils import load_mock_json, mock_responses @mock_responses( diff --git a/tests/test_status.py b/tests/test_status.py deleted file mode 100644 index c23cbb4..0000000 --- a/tests/test_status.py +++ /dev/null @@ -1,89 +0,0 @@ -"""Tests vehicle status using respx for mocking HTTP requests.""" - -import pytest -from respx import MockRouter - -import vehiclepass -from vehiclepass import units -from vehiclepass.constants import AUTONOMIC_AUTH_URL, AUTONOMIC_TELEMETRY_BASE_URL -from vehiclepass.tire_pressure import TirePressure - -from .conftest import mock_responses - - -@mock_responses(status="status/baseline.json") -def test_tire_pressure(vehicle: vehiclepass.Vehicle) -> None: - """Test tire pressure properties.""" - assert isinstance(vehicle.tire_pressure, TirePressure) - assert vehicle.tire_pressure.front_left.psi == 39.45 # type: ignore - assert str(vehicle.tire_pressure.front_left) == "39.45 psi" # type: ignore - assert vehicle.tire_pressure.front_left.kpa == 272.0 # type: ignore - assert vehicle.tire_pressure.front_right.psi == 40.18 # type: ignore - assert str(vehicle.tire_pressure.front_right) == "40.18 psi" # type: ignore - assert vehicle.tire_pressure.front_right.kpa == 277.0 # type: ignore - assert vehicle.tire_pressure.rear_left.psi == 39.89 # type: ignore - assert str(vehicle.tire_pressure.rear_left) == "39.89 psi" # type: ignore - assert vehicle.tire_pressure.rear_left.kpa == 275.0 # type: ignore - assert vehicle.tire_pressure.rear_right.psi == 39.89 # type: ignore - assert str(vehicle.tire_pressure.rear_right) == "39.89 psi" # type: ignore - assert vehicle.tire_pressure.rear_right.kpa == 275.0 # type: ignore - - -@mock_responses(status="status/baseline.json") -def test_temperature(vehicle: vehiclepass.Vehicle) -> None: - """Test temperature properties.""" - assert isinstance(vehicle.outside_temp, units.Temperature) - assert vehicle.outside_temp.c == 0.0 - assert vehicle.outside_temp.f == 32.0 - assert str(vehicle.outside_temp) == "32.0°F" - - assert isinstance(vehicle.engine_coolant_temp, units.Temperature) - assert vehicle.engine_coolant_temp.c == 89.0 - assert vehicle.engine_coolant_temp.f == 192.2 - assert str(vehicle.engine_coolant_temp) == "192.2°F" - - -@mock_responses(status="status/baseline.json") -def test_odometer(vehicle: vehiclepass.Vehicle) -> None: - """Test odometer properties.""" - assert isinstance(vehicle.odometer, units.Distance) - assert vehicle.odometer.km == 105547.0 - assert vehicle.odometer.mi == 65583.84 - assert str(vehicle.odometer) == "65583.84 mi" - - -@mock_responses(status="status/baseline.json") -def test_fuel(vehicle: vehiclepass.Vehicle) -> None: - """Test fuel properties.""" - assert isinstance(vehicle.fuel_level, units.Percentage) - assert vehicle.fuel_level.percentage == 0.72717624 - assert vehicle.fuel_level.percent == 0.73 - assert str(vehicle.fuel_level) == "73.0%" - - -@mock_responses(status="status/remotely_started.json") -def test_status_remotely_started(vehicle: vehiclepass.Vehicle) -> None: - """Test status when vehicle is remotely started.""" - assert vehicle.is_running is True - assert vehicle.is_remotely_started is True - assert vehicle.shutoff_countdown.seconds == 851.0 - assert vehicle.shutoff_countdown.human_readable == "14m 11s" - assert vehicle.is_ignition_started is False - - -@pytest.mark.parametrize("temp_c, temp_f", [(0, 32), (20, 68), (37, 98.6), (-10, 14)]) -def test_temperature_conversion(vehicle: vehiclepass.Vehicle, mock_router: MockRouter, temp_c: float, temp_f: float): - """Test that temperature conversions work correctly.""" - mock_router.post(AUTONOMIC_AUTH_URL).respond( - json={"access_token": "mock-token-12345", "token_type": "Bearer", "expires_in": 3600} - ) - - mock_router.get(f"{AUTONOMIC_TELEMETRY_BASE_URL}/MOCK12345").respond( - json={"metrics": {"outsideTemperature": {"value": float(temp_c)}}} - ) - - temp = vehicle.outside_temp - assert temp.c == temp_c - assert round(temp.f, 1) == temp_f - - mock_router.reset() diff --git a/tests/test_vehicle_status.py b/tests/test_vehicle_status.py new file mode 100644 index 0000000..a4acf8a --- /dev/null +++ b/tests/test_vehicle_status.py @@ -0,0 +1,165 @@ +"""Tests vehicle status using respx for mocking HTTP requests.""" + +import pytest +from pydantic_extra_types.coordinate import Coordinate, Latitude, Longitude +from respx import MockRouter + +import vehiclepass +from tests.utils import mock_responses +from vehiclepass import units +from vehiclepass.constants import AUTONOMIC_AUTH_URL, AUTONOMIC_TELEMETRY_BASE_URL +from vehiclepass.seatbelts import SeatBelts +from vehiclepass.tires import Tires + + +@mock_responses(status="status/baseline.json") +def test_get_metric_value(vehicle: vehiclepass.Vehicle) -> None: + """Test that _get_metric_value returns the correct value.""" + assert isinstance(vehicle._get_metric_value("batteryVoltage"), float) + assert isinstance(vehicle._get_metric_value("compassDirection"), str) + with pytest.raises(vehiclepass.StatusError): + vehicle._get_metric_value("batteryVoltage", str) # Incorrect type + vehicle._get_metric_value("invalid_metric") + + +@mock_responses(status="status/baseline.json") +def test_tire_pressure(vehicle: vehiclepass.Vehicle) -> None: + """Test tire pressure properties.""" + assert isinstance(vehicle.tires, Tires) + assert isinstance(vehicle.tyres, Tires) + + # Front left + assert getattr(vehicle.tires, "front_left").pressure.bar == 2.72 + assert getattr(vehicle.tires, "front_left").pressure.kpa == 272.0 + assert getattr(vehicle.tires, "front_left").pressure.psi == 39.45 + assert str(getattr(vehicle.tires, "front_left").pressure) == "39.45 psi" + assert getattr(vehicle.tires, "front_left").pressure.data == {"psi": 39.45, "bar": 2.72, "kpa": 272.0} + + # Front right + assert getattr(vehicle.tires, "front_right").pressure.bar == 2.77 + assert getattr(vehicle.tires, "front_right").pressure.kpa == 277.0 + assert getattr(vehicle.tires, "front_right").pressure.psi == 40.18 + assert str(getattr(vehicle.tires, "front_right").pressure) == "40.18 psi" + assert getattr(vehicle.tires, "front_right").pressure.data == {"psi": 40.18, "bar": 2.77, "kpa": 277.0} + + # Rear left + assert getattr(vehicle.tires, "rear_left").pressure.psi == 39.89 + assert getattr(vehicle.tires, "rear_left").pressure.bar == 2.75 + assert getattr(vehicle.tires, "rear_left").pressure.kpa == 275.0 + assert str(getattr(vehicle.tires, "rear_left").pressure) == "39.89 psi" + assert getattr(vehicle.tires, "rear_left").pressure.data == {"psi": 39.89, "bar": 2.75, "kpa": 275.0} + + # Rear right + assert getattr(vehicle.tires, "rear_right").pressure.bar == 2.75 + assert getattr(vehicle.tires, "rear_right").pressure.kpa == 275.0 + assert getattr(vehicle.tires, "rear_right").pressure.psi == 39.89 + assert getattr(vehicle.tires, "rear_right").pressure.data == {"psi": 39.89, "bar": 2.75, "kpa": 275.0} + assert str(getattr(vehicle.tires, "rear_right").pressure) == "39.89 psi" + + +@mock_responses(status="status/baseline.json") +def test_tire_status(vehicle: vehiclepass.Vehicle) -> None: + """Test tire status properties.""" + assert isinstance(vehicle.tires, Tires) + assert getattr(vehicle.tires, "front_left").status == "NORMAL" + assert getattr(vehicle.tires, "front_right").status == "NORMAL" + assert getattr(vehicle.tires, "rear_left").status == "NORMAL" + assert getattr(vehicle.tires, "rear_right").status == "NORMAL" + + +@mock_responses(status="status/baseline.json") +def test_seatbelts(vehicle: vehiclepass.Vehicle) -> None: + """Test seatbelt status.""" + assert isinstance(vehicle.seatbelts, SeatBelts) + assert getattr(vehicle.seatbelts, "driver") == "UNBUCKLED" + assert getattr(vehicle.seatbelts, "passenger") == "UNBUCKLED" + + +@mock_responses(status="status/baseline.json") +def test_tires_system_status(vehicle: vehiclepass.Vehicle) -> None: + """Test tire pressure system status.""" + assert vehicle.tires.system_status == "NORMAL_OPERATION" + assert vehicle.tpms.system_status == "NORMAL_OPERATION" + assert vehicle.tyres.system_status == "NORMAL_OPERATION" + assert vehicle.tires.data == { + "front_left": {"pressure": {"psi": 39.45, "bar": 2.72, "kpa": 272.0}, "status": "NORMAL"}, + "front_right": {"pressure": {"psi": 40.18, "bar": 2.77, "kpa": 277.0}, "status": "NORMAL"}, + "rear_left": {"pressure": {"psi": 39.89, "bar": 2.75, "kpa": 275.0}, "status": "NORMAL"}, + "rear_right": {"pressure": {"psi": 39.89, "bar": 2.75, "kpa": 275.0}, "status": "NORMAL"}, + } + + +@mock_responses(status="status/baseline.json") +def test_temperature(vehicle: vehiclepass.Vehicle) -> None: + """Test temperature properties.""" + assert isinstance(vehicle.outside_temp, units.Temperature) + assert vehicle.outside_temp.c == 0.0 + assert vehicle.outside_temp.f == 32.0 + assert str(vehicle.outside_temp) == "32.0°F" + + assert isinstance(vehicle.engine_coolant_temp, units.Temperature) + assert vehicle.engine_coolant_temp.c == 89.0 + assert vehicle.engine_coolant_temp.f == 192.2 + assert str(vehicle.engine_coolant_temp) == "192.2°F" + + +@mock_responses(status="status/baseline.json") +def test_odometer(vehicle: vehiclepass.Vehicle) -> None: + """Test odometer properties.""" + assert isinstance(vehicle.odometer, units.Distance) + assert vehicle.odometer.km == 105547.0 + assert vehicle.odometer.mi == 65583.84 + assert str(vehicle.odometer) == "65583.84 mi" + + +@mock_responses(status="status/baseline.json") +def test_oil_life_remaining(vehicle: vehiclepass.Vehicle) -> None: + """Test oil life remaining.""" + assert isinstance(vehicle.oil_life_remaining, units.Percentage) + assert vehicle.oil_life_remaining.value == 0.51 + assert str(vehicle.oil_life_remaining) == "51.0%" + + +@mock_responses(status="status/baseline.json") +def test_fuel(vehicle: vehiclepass.Vehicle) -> None: + """Test fuel properties.""" + assert isinstance(vehicle.fuel_level, units.Percentage) + assert vehicle.fuel_level.value == 0.72717624 + assert vehicle.fuel_level.percent == 0.7272 + assert str(vehicle.fuel_level) == "72.72%" + + +@mock_responses(status="status/remotely_started.json") +def test_status_remotely_started(vehicle: vehiclepass.Vehicle) -> None: + """Test status when vehicle is remotely started.""" + assert vehicle.is_running is True + assert vehicle.is_remotely_started is True + assert vehicle.shutoff_countdown.seconds == 851.0 + assert vehicle.shutoff_countdown.human_readable == "14m 11s" + assert vehicle.is_ignition_started is False + + +@mock_responses(status="status/baseline.json") +def test_position(vehicle: vehiclepass.Vehicle) -> None: + """Test vehicle position (GPS coordinates).""" + assert isinstance(vehicle.position, Coordinate) + assert vehicle.position.latitude == Latitude(42.31474) + assert vehicle.position.longitude == Longitude(-83.21043) + + +@pytest.mark.parametrize("temp_c, temp_f", [(0, 32), (20, 68), (37, 98.6), (-10, 14)]) +def test_temperature_conversion(vehicle: vehiclepass.Vehicle, mock_router: MockRouter, temp_c: float, temp_f: float): + """Test that temperature conversions work correctly.""" + mock_router.post(AUTONOMIC_AUTH_URL).respond( + json={"access_token": "mock-token-12345", "token_type": "Bearer", "expires_in": 3600} + ) + + mock_router.get(f"{AUTONOMIC_TELEMETRY_BASE_URL}/MOCK12345").respond( + json={"metrics": {"outsideTemperature": {"value": float(temp_c)}}} + ) + + temp = vehicle.outside_temp + assert temp.c == temp_c + assert round(temp.f, 1) == temp_f + + mock_router.reset() diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..cbd8549 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,174 @@ +"""Test utilities.""" + +import functools +import json +import logging +import re +from collections.abc import Callable, Iterable +from itertools import chain, repeat +from pathlib import Path +from typing import Any, TypeVar, Union + +import httpx +import respx + +from vehiclepass.constants import ( + AUTONOMIC_AUTH_URL, + AUTONOMIC_COMMAND_BASE_URL, + AUTONOMIC_TELEMETRY_BASE_URL, + FORDPASS_AUTH_URL, +) + +T = TypeVar("T") + +MOCK_RESPONSES_DIR = Path(__file__).parent / "fixtures" / "responses" + + +def pytest_configure(config): + """Configure pytest.""" + logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)") + + +def load_mock_json(file_path: Union[str, Path]) -> dict[str, Any]: + """Load mock data from a JSON file. + + Args: + file_path: Path to the JSON file + + Returns: + Dict containing the loaded JSON data + """ + original_path = Path(file_path) + final_path = original_path + + # Check if it's a relative path and try to find it in mock_data directory + if not original_path.is_absolute(): + if original_path.exists(): + final_path = original_path + else: + relative_path = MOCK_RESPONSES_DIR / original_path + if relative_path.exists(): + final_path = relative_path + else: + raise FileNotFoundError(f"Mock data file not found: {original_path}") + + with open(final_path) as f: + return json.load(f) + + +def mock_responses( + status: Union[httpx.Response, str, Path, list[Union[str, Path, httpx.Response]], dict[str, Any], None] = None, + commands: Union[ + dict[str, Union[httpx.Response, str, Path, list[Union[str, Path, httpx.Response]], dict[str, Any]]], None + ] = None, + auth_token: str = "mock-token-12345", +): + """Test decorator that mocks HTTP API responses. + + Intercepts network requests for the FordPass API and returns predefined mock + responses instead of making actual API calls. + + Args: + status: Controls responses for status/telemetry endpoint requests. + - httpx.Response: Used directly as the response + - str or Path: Path to a JSON file, loaded and returned with 200 status code + - dict: Raw JSON data, returned with 200 status code + - list: Multiple responses returned in sequence for consecutive calls + (last item repeats for any additional calls) + - None: Returns a default baseline status from "status/baseline.json" + + commands: Controls responses for vehicle command endpoint requests. + - dict mapping command names to response definitions + - Each response can be a str/Path to JSON file, raw dict, or a list of + responses returned in sequence (like status) + - None: All command requests return 404 + + auth_token: The mock authentication token to use for all authorized requests. + + Returns: + A decorated test function that runs within the mocked HTTP environment. + + Example: + @mock_responses( + status="tests/mocks/vehicle_status.json", + commands={ + "unlock": "tests/mocks/unlock_success.json", + "lock": [ + "tests/mocks/command_accepted.json", + "tests/mocks/lock_complete.json" + ] + } + ) + def test_vehicle_commands(self): + # Test code that makes API calls, which will now use mock responses + ... + """ + + def decorator(test_func: Callable[..., T]) -> Callable[..., T]: + def get_response_data(source): + if isinstance(source, (str, Path)): + return load_mock_json(source) + return source + + command_indexes = {} + + def get_response(source_data, index_tracker): + # For non-list sources, just return the data directly + if not isinstance(source_data, list): + return get_response_data(source_data) + + # For lists, return the current item or the last one if we've reached the end + current = index_tracker["current"] + if current < len(source_data) - 1: + index_tracker["current"] = current + 1 + return get_response_data(source_data[current]) + # Return the last item for all subsequent calls + return get_response_data(source_data[-1]) + + def status_handler() -> Union[Iterable[httpx.Response], httpx.Response]: + if isinstance(status, (str, Path)): + return httpx.Response(status_code=200, json=load_mock_json(status)) + if isinstance(status, list): + responses = [ + s if isinstance(s, httpx.Response) else httpx.Response(status_code=200, json=load_mock_json(s)) + for s in status + ] + return chain(responses, repeat(responses[-1])) + if isinstance(status, dict): + return httpx.Response(status_code=200, json=status) + return httpx.Response(status_code=200, json=load_mock_json("status/baseline.json")) + + def command_handler(request): + if commands is None: + return httpx.Response(status_code=404) + + command = json.loads(request.content.decode("utf-8")).get("type") + if command not in commands: + return httpx.Response(status_code=404) + + if command not in command_indexes: + command_indexes[command] = {"current": 0} + + response_data = get_response(commands[command], command_indexes[command]) + return ( + response_data + if isinstance(response_data, httpx.Response) + else httpx.Response(status_code=200, json=response_data) + ) + + @functools.wraps(test_func) + def wrapper(*args, **kwargs): + with respx.mock(assert_all_called=False) as mock: + mock.post(FORDPASS_AUTH_URL).respond( + json={"access_token": auth_token, "token_type": "Bearer", "expires_in": 3600} + ) + mock.post(AUTONOMIC_AUTH_URL).respond( + json={"access_token": auth_token, "token_type": "Bearer", "expires_in": 3600} + ) + mock.get(re.compile(rf"{AUTONOMIC_TELEMETRY_BASE_URL}/*")).side_effect = status_handler() # type: ignore + mock.post(re.compile(rf"{AUTONOMIC_COMMAND_BASE_URL}/*")).side_effect = command_handler + return test_func(*args, **kwargs) + + return wrapper + + return decorator diff --git a/uv.lock b/uv.lock index 97d4bca..dae74d2 100644 --- a/uv.lock +++ b/uv.lock @@ -208,8 +208,8 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "decorator" }, { name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "ipython", version = "8.34.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "ipython", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "ipython", version = "8.35.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "ipython", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3d/1b/7e07e7b752017f7693a0f4d41c13e5ca29ce8cbcfdcc1fd6c4ad8c0a27a0/ipdb-0.13.13.tar.gz", hash = "sha256:e3ac6018ef05126d442af680aad863006ec19d02290561ac88b8b1c0b0cfc726", size = 17042 } @@ -244,7 +244,7 @@ wheels = [ [[package]] name = "ipython" -version = "8.34.0" +version = "8.35.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version == '3.10.*'", @@ -262,14 +262,14 @@ dependencies = [ { name = "traitlets", marker = "python_full_version == '3.10.*'" }, { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/13/18/1a60aa62e9d272fcd7e658a89e1c148da10e1a5d38edcbcd834b52ca7492/ipython-8.34.0.tar.gz", hash = "sha256:c31d658e754673ecc6514583e7dda8069e47136eb62458816b7d1e6625948b5a", size = 5508477 } +sdist = { url = "https://files.pythonhosted.org/packages/0c/77/7d1501e8b539b179936e0d5969b578ed23887be0ab8c63e0120b825bda3e/ipython-8.35.0.tar.gz", hash = "sha256:d200b7d93c3f5883fc36ab9ce28a18249c7706e51347681f80a0aef9895f2520", size = 5605027 } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/78/45615356bb973904856808183ae2a5fba1f360e9d682314d79766f4b88f2/ipython-8.34.0-py3-none-any.whl", hash = "sha256:0419883fa46e0baa182c5d50ebb8d6b49df1889fdb70750ad6d8cfe678eda6e3", size = 826731 }, + { url = "https://files.pythonhosted.org/packages/91/bf/17ffca8c8b011d0bac90adb5d4e720cb3ae1fe5ccfdfc14ca31f827ee320/ipython-8.35.0-py3-none-any.whl", hash = "sha256:e6b7470468ba6f1f0a7b116bb688a3ece2f13e2f94138e508201fad677a788ba", size = 830880 }, ] [[package]] name = "ipython" -version = "9.0.2" +version = "9.1.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.11'", @@ -287,9 +287,9 @@ dependencies = [ { name = "traitlets", marker = "python_full_version >= '3.11'" }, { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/ce/012a0f40ca58a966f87a6e894d6828e2817657cbdf522b02a5d3a87d92ce/ipython-9.0.2.tar.gz", hash = "sha256:ec7b479e3e5656bf4f58c652c120494df1820f4f28f522fb7ca09e213c2aab52", size = 4366102 } +sdist = { url = "https://files.pythonhosted.org/packages/70/9a/6b8984bedc990f3a4aa40ba8436dea27e23d26a64527de7c2e5e12e76841/ipython-9.1.0.tar.gz", hash = "sha256:a47e13a5e05e02f3b8e1e7a0f9db372199fe8c3763532fe7a1e0379e4e135f16", size = 4373688 } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/3a/917cb9e72f4e1a4ea13c862533205ae1319bd664119189ee5cc9e4e95ebf/ipython-9.0.2-py3-none-any.whl", hash = "sha256:143ef3ea6fb1e1bffb4c74b114051de653ffb7737a3f7ab1670e657ca6ae8c44", size = 600524 }, + { url = "https://files.pythonhosted.org/packages/b2/9d/4ff2adf55d1b6e3777b0303fdbe5b723f76e46cba4a53a32fe82260d2077/ipython-9.1.0-py3-none-any.whl", hash = "sha256:2df07257ec2f84a6b346b8d83100bcf8fa501c6e01ab75cd3799b0bb253b3d2a", size = 604053 }, ] [[package]] @@ -479,7 +479,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.11.1" +version = "2.11.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -487,118 +487,131 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/a3/698b87a4d4d303d7c5f62ea5fbf7a79cab236ccfbd0a17847b7f77f8163e/pydantic-2.11.1.tar.gz", hash = "sha256:442557d2910e75c991c39f4b4ab18963d57b9b55122c8b2a9cd176d8c29ce968", size = 782817 } +sdist = { url = "https://files.pythonhosted.org/packages/b0/41/832125a41fe098b58d1fdd04ae819b4dc6b34d6b09ed78304fd93d4bc051/pydantic-2.11.2.tar.gz", hash = "sha256:2138628e050bd7a1e70b91d4bf4a91167f4ad76fdb83209b107c8d84b854917e", size = 784742 } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/12/f9221a949f2419e2e23847303c002476c26fbcfd62dc7f3d25d0bec5ca99/pydantic-2.11.1-py3-none-any.whl", hash = "sha256:5b6c415eee9f8123a14d859be0c84363fec6b1feb6b688d6435801230b56e0b8", size = 442648 }, + { url = "https://files.pythonhosted.org/packages/bf/c2/0f3baea344d0b15e35cb3e04ad5b953fa05106b76efbf4c782a3f47f22f5/pydantic-2.11.2-py3-none-any.whl", hash = "sha256:7f17d25846bcdf89b670a86cdfe7b29a9f1c9ca23dee154221c9aa81845cfca7", size = 443295 }, ] [[package]] name = "pydantic-core" -version = "2.33.0" +version = "2.33.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/05/91ce14dfd5a3a99555fce436318cc0fd1f08c4daa32b3248ad63669ea8b4/pydantic_core-2.33.0.tar.gz", hash = "sha256:40eb8af662ba409c3cbf4a8150ad32ae73514cd7cb1f1a2113af39763dd616b3", size = 434080 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/29/43/0649ad07e66b36a3fb21442b425bd0348ac162c5e686b36471f363201535/pydantic_core-2.33.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71dffba8fe9ddff628c68f3abd845e91b028361d43c5f8e7b3f8b91d7d85413e", size = 2042968 }, - { url = "https://files.pythonhosted.org/packages/a0/a6/975fea4774a459e495cb4be288efd8b041ac756a0a763f0b976d0861334b/pydantic_core-2.33.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:abaeec1be6ed535a5d7ffc2e6c390083c425832b20efd621562fbb5bff6dc518", size = 1860347 }, - { url = "https://files.pythonhosted.org/packages/aa/49/7858dadad305101a077ec4d0c606b6425a2b134ea8d858458a6d287fd871/pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:759871f00e26ad3709efc773ac37b4d571de065f9dfb1778012908bcc36b3a73", size = 1910060 }, - { url = "https://files.pythonhosted.org/packages/8d/4f/6522527911d9c5fe6d76b084d8b388d5c84b09d113247b39f91937500b34/pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dcfebee69cd5e1c0b76a17e17e347c84b00acebb8dd8edb22d4a03e88e82a207", size = 1997129 }, - { url = "https://files.pythonhosted.org/packages/75/d0/06f396da053e3d73001ea4787e56b4d7132a87c0b5e2e15a041e808c35cd/pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b1262b912435a501fa04cd213720609e2cefa723a07c92017d18693e69bf00b", size = 2140389 }, - { url = "https://files.pythonhosted.org/packages/f5/6b/b9ff5b69cd4ef007cf665463f3be2e481dc7eb26c4a55b2f57a94308c31a/pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4726f1f3f42d6a25678c67da3f0b10f148f5655813c5aca54b0d1742ba821b8f", size = 2754237 }, - { url = "https://files.pythonhosted.org/packages/53/80/b4879de375cdf3718d05fcb60c9aa1f119d28e261dafa51b6a69c78f7178/pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e790954b5093dff1e3a9a2523fddc4e79722d6f07993b4cd5547825c3cbf97b5", size = 2007433 }, - { url = "https://files.pythonhosted.org/packages/46/24/54054713dc0af98a94eab37e0f4294dfd5cd8f70b2ca9dcdccd15709fd7e/pydantic_core-2.33.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:34e7fb3abe375b5c4e64fab75733d605dda0f59827752debc99c17cb2d5f3276", size = 2123980 }, - { url = "https://files.pythonhosted.org/packages/3a/4c/257c1cb89e14cfa6e95ebcb91b308eb1dd2b348340ff76a6e6fcfa9969e1/pydantic_core-2.33.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ecb158fb9b9091b515213bed3061eb7deb1d3b4e02327c27a0ea714ff46b0760", size = 2087433 }, - { url = "https://files.pythonhosted.org/packages/0c/62/927df8a39ad78ef7b82c5446e01dec9bb0043e1ad71d8f426062f5f014db/pydantic_core-2.33.0-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:4d9149e7528af8bbd76cc055967e6e04617dcb2a2afdaa3dea899406c5521faa", size = 2260242 }, - { url = "https://files.pythonhosted.org/packages/74/f2/389414f7c77a100954e84d6f52a82bd1788ae69db72364376d8a73b38765/pydantic_core-2.33.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e81a295adccf73477220e15ff79235ca9dcbcee4be459eb9d4ce9a2763b8386c", size = 2258227 }, - { url = "https://files.pythonhosted.org/packages/53/99/94516313e15d906a1264bb40faf24a01a4af4e2ca8a7c10dd173b6513c5a/pydantic_core-2.33.0-cp310-cp310-win32.whl", hash = "sha256:f22dab23cdbce2005f26a8f0c71698457861f97fc6318c75814a50c75e87d025", size = 1925523 }, - { url = "https://files.pythonhosted.org/packages/7d/67/cc789611c6035a0b71305a1ec6ba196256ced76eba8375f316f840a70456/pydantic_core-2.33.0-cp310-cp310-win_amd64.whl", hash = "sha256:9cb2390355ba084c1ad49485d18449b4242da344dea3e0fe10babd1f0db7dcfc", size = 1951872 }, - { url = "https://files.pythonhosted.org/packages/f0/93/9e97af2619b4026596487a79133e425c7d3c374f0a7f100f3d76bcdf9c83/pydantic_core-2.33.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a608a75846804271cf9c83e40bbb4dab2ac614d33c6fd5b0c6187f53f5c593ef", size = 2042784 }, - { url = "https://files.pythonhosted.org/packages/42/b4/0bba8412fd242729feeb80e7152e24f0e1a1c19f4121ca3d4a307f4e6222/pydantic_core-2.33.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e1c69aa459f5609dec2fa0652d495353accf3eda5bdb18782bc5a2ae45c9273a", size = 1858179 }, - { url = "https://files.pythonhosted.org/packages/69/1f/c1c40305d929bd08af863df64b0a26203b70b352a1962d86f3bcd52950fe/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9ec80eb5a5f45a2211793f1c4aeddff0c3761d1c70d684965c1807e923a588b", size = 1909396 }, - { url = "https://files.pythonhosted.org/packages/0f/99/d2e727375c329c1e652b5d450fbb9d56e8c3933a397e4bd46e67c68c2cd5/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e925819a98318d17251776bd3d6aa9f3ff77b965762155bdad15d1a9265c4cfd", size = 1998264 }, - { url = "https://files.pythonhosted.org/packages/9c/2e/3119a33931278d96ecc2e9e1b9d50c240636cfeb0c49951746ae34e4de74/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bf68bb859799e9cec3d9dd8323c40c00a254aabb56fe08f907e437005932f2b", size = 2140588 }, - { url = "https://files.pythonhosted.org/packages/35/bd/9267bd1ba55f17c80ef6cb7e07b3890b4acbe8eb6014f3102092d53d9300/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1b2ea72dea0825949a045fa4071f6d5b3d7620d2a208335207793cf29c5a182d", size = 2746296 }, - { url = "https://files.pythonhosted.org/packages/6f/ed/ef37de6478a412ee627cbebd73e7b72a680f45bfacce9ff1199de6e17e88/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1583539533160186ac546b49f5cde9ffc928062c96920f58bd95de32ffd7bffd", size = 2005555 }, - { url = "https://files.pythonhosted.org/packages/dd/84/72c8d1439585d8ee7bc35eb8f88a04a4d302ee4018871f1f85ae1b0c6625/pydantic_core-2.33.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:23c3e77bf8a7317612e5c26a3b084c7edeb9552d645742a54a5867635b4f2453", size = 2124452 }, - { url = "https://files.pythonhosted.org/packages/a7/8f/cb13de30c6a3e303423751a529a3d1271c2effee4b98cf3e397a66ae8498/pydantic_core-2.33.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a7a7f2a3f628d2f7ef11cb6188bcf0b9e1558151d511b974dfea10a49afe192b", size = 2087001 }, - { url = "https://files.pythonhosted.org/packages/83/d0/e93dc8884bf288a63fedeb8040ac8f29cb71ca52e755f48e5170bb63e55b/pydantic_core-2.33.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:f1fb026c575e16f673c61c7b86144517705865173f3d0907040ac30c4f9f5915", size = 2261663 }, - { url = "https://files.pythonhosted.org/packages/4c/ba/4b7739c95efa0b542ee45fd872c8f6b1884ab808cf04ce7ac6621b6df76e/pydantic_core-2.33.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:635702b2fed997e0ac256b2cfbdb4dd0bf7c56b5d8fba8ef03489c03b3eb40e2", size = 2257786 }, - { url = "https://files.pythonhosted.org/packages/cc/98/73cbca1d2360c27752cfa2fcdcf14d96230e92d7d48ecd50499865c56bf7/pydantic_core-2.33.0-cp311-cp311-win32.whl", hash = "sha256:07b4ced28fccae3f00626eaa0c4001aa9ec140a29501770a88dbbb0966019a86", size = 1925697 }, - { url = "https://files.pythonhosted.org/packages/9a/26/d85a40edeca5d8830ffc33667d6fef329fd0f4bc0c5181b8b0e206cfe488/pydantic_core-2.33.0-cp311-cp311-win_amd64.whl", hash = "sha256:4927564be53239a87770a5f86bdc272b8d1fbb87ab7783ad70255b4ab01aa25b", size = 1949859 }, - { url = "https://files.pythonhosted.org/packages/7e/0b/5a381605f0b9870465b805f2c86c06b0a7c191668ebe4117777306c2c1e5/pydantic_core-2.33.0-cp311-cp311-win_arm64.whl", hash = "sha256:69297418ad644d521ea3e1aa2e14a2a422726167e9ad22b89e8f1130d68e1e9a", size = 1907978 }, - { url = "https://files.pythonhosted.org/packages/a9/c4/c9381323cbdc1bb26d352bc184422ce77c4bc2f2312b782761093a59fafc/pydantic_core-2.33.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6c32a40712e3662bebe524abe8abb757f2fa2000028d64cc5a1006016c06af43", size = 2025127 }, - { url = "https://files.pythonhosted.org/packages/6f/bd/af35278080716ecab8f57e84515c7dc535ed95d1c7f52c1c6f7b313a9dab/pydantic_core-2.33.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ec86b5baa36f0a0bfb37db86c7d52652f8e8aa076ab745ef7725784183c3fdd", size = 1851687 }, - { url = "https://files.pythonhosted.org/packages/12/e4/a01461225809c3533c23bd1916b1e8c2e21727f0fea60ab1acbffc4e2fca/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4deac83a8cc1d09e40683be0bc6d1fa4cde8df0a9bf0cda5693f9b0569ac01b6", size = 1892232 }, - { url = "https://files.pythonhosted.org/packages/51/17/3d53d62a328fb0a49911c2962036b9e7a4f781b7d15e9093c26299e5f76d/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:175ab598fb457a9aee63206a1993874badf3ed9a456e0654273e56f00747bbd6", size = 1977896 }, - { url = "https://files.pythonhosted.org/packages/30/98/01f9d86e02ec4a38f4b02086acf067f2c776b845d43f901bd1ee1c21bc4b/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f36afd0d56a6c42cf4e8465b6441cf546ed69d3a4ec92724cc9c8c61bd6ecf4", size = 2127717 }, - { url = "https://files.pythonhosted.org/packages/3c/43/6f381575c61b7c58b0fd0b92134c5a1897deea4cdfc3d47567b3ff460a4e/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a98257451164666afafc7cbf5fb00d613e33f7e7ebb322fbcd99345695a9a61", size = 2680287 }, - { url = "https://files.pythonhosted.org/packages/01/42/c0d10d1451d161a9a0da9bbef023b8005aa26e9993a8cc24dc9e3aa96c93/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecc6d02d69b54a2eb83ebcc6f29df04957f734bcf309d346b4f83354d8376862", size = 2008276 }, - { url = "https://files.pythonhosted.org/packages/20/ca/e08df9dba546905c70bae44ced9f3bea25432e34448d95618d41968f40b7/pydantic_core-2.33.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a69b7596c6603afd049ce7f3835bcf57dd3892fc7279f0ddf987bebed8caa5a", size = 2115305 }, - { url = "https://files.pythonhosted.org/packages/03/1f/9b01d990730a98833113581a78e595fd40ed4c20f9693f5a658fb5f91eff/pydantic_core-2.33.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ea30239c148b6ef41364c6f51d103c2988965b643d62e10b233b5efdca8c0099", size = 2068999 }, - { url = "https://files.pythonhosted.org/packages/20/18/fe752476a709191148e8b1e1139147841ea5d2b22adcde6ee6abb6c8e7cf/pydantic_core-2.33.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:abfa44cf2f7f7d7a199be6c6ec141c9024063205545aa09304349781b9a125e6", size = 2241488 }, - { url = "https://files.pythonhosted.org/packages/81/22/14738ad0a0bf484b928c9e52004f5e0b81dd8dabbdf23b843717b37a71d1/pydantic_core-2.33.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20d4275f3c4659d92048c70797e5fdc396c6e4446caf517ba5cad2db60cd39d3", size = 2248430 }, - { url = "https://files.pythonhosted.org/packages/e8/27/be7571e215ac8d321712f2433c445b03dbcd645366a18f67b334df8912bc/pydantic_core-2.33.0-cp312-cp312-win32.whl", hash = "sha256:918f2013d7eadea1d88d1a35fd4a1e16aaf90343eb446f91cb091ce7f9b431a2", size = 1908353 }, - { url = "https://files.pythonhosted.org/packages/be/3a/be78f28732f93128bd0e3944bdd4b3970b389a1fbd44907c97291c8dcdec/pydantic_core-2.33.0-cp312-cp312-win_amd64.whl", hash = "sha256:aec79acc183865bad120b0190afac467c20b15289050648b876b07777e67ea48", size = 1955956 }, - { url = "https://files.pythonhosted.org/packages/21/26/b8911ac74faa994694b76ee6a22875cc7a4abea3c381fdba4edc6c6bef84/pydantic_core-2.33.0-cp312-cp312-win_arm64.whl", hash = "sha256:5461934e895968655225dfa8b3be79e7e927e95d4bd6c2d40edd2fa7052e71b6", size = 1903259 }, - { url = "https://files.pythonhosted.org/packages/79/20/de2ad03ce8f5b3accf2196ea9b44f31b0cd16ac6e8cfc6b21976ed45ec35/pydantic_core-2.33.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f00e8b59e1fc8f09d05594aa7d2b726f1b277ca6155fc84c0396db1b373c4555", size = 2032214 }, - { url = "https://files.pythonhosted.org/packages/f9/af/6817dfda9aac4958d8b516cbb94af507eb171c997ea66453d4d162ae8948/pydantic_core-2.33.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a73be93ecef45786d7d95b0c5e9b294faf35629d03d5b145b09b81258c7cd6d", size = 1852338 }, - { url = "https://files.pythonhosted.org/packages/44/f3/49193a312d9c49314f2b953fb55740b7c530710977cabe7183b8ef111b7f/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff48a55be9da6930254565ff5238d71d5e9cd8c5487a191cb85df3bdb8c77365", size = 1896913 }, - { url = "https://files.pythonhosted.org/packages/06/e0/c746677825b2e29a2fa02122a8991c83cdd5b4c5f638f0664d4e35edd4b2/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a4ea04195638dcd8c53dadb545d70badba51735b1594810e9768c2c0b4a5da", size = 1986046 }, - { url = "https://files.pythonhosted.org/packages/11/ec/44914e7ff78cef16afb5e5273d480c136725acd73d894affdbe2a1bbaad5/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41d698dcbe12b60661f0632b543dbb119e6ba088103b364ff65e951610cb7ce0", size = 2128097 }, - { url = "https://files.pythonhosted.org/packages/fe/f5/c6247d424d01f605ed2e3802f338691cae17137cee6484dce9f1ac0b872b/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae62032ef513fe6281ef0009e30838a01057b832dc265da32c10469622613885", size = 2681062 }, - { url = "https://files.pythonhosted.org/packages/f0/85/114a2113b126fdd7cf9a9443b1b1fe1b572e5bd259d50ba9d5d3e1927fa9/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f225f3a3995dbbc26affc191d0443c6c4aa71b83358fd4c2b7d63e2f6f0336f9", size = 2007487 }, - { url = "https://files.pythonhosted.org/packages/e6/40/3c05ed28d225c7a9acd2b34c5c8010c279683a870219b97e9f164a5a8af0/pydantic_core-2.33.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5bdd36b362f419c78d09630cbaebc64913f66f62bda6d42d5fbb08da8cc4f181", size = 2121382 }, - { url = "https://files.pythonhosted.org/packages/8a/22/e70c086f41eebd323e6baa92cc906c3f38ddce7486007eb2bdb3b11c8f64/pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2a0147c0bef783fd9abc9f016d66edb6cac466dc54a17ec5f5ada08ff65caf5d", size = 2072473 }, - { url = "https://files.pythonhosted.org/packages/3e/84/d1614dedd8fe5114f6a0e348bcd1535f97d76c038d6102f271433cd1361d/pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:c860773a0f205926172c6644c394e02c25421dc9a456deff16f64c0e299487d3", size = 2249468 }, - { url = "https://files.pythonhosted.org/packages/b0/c0/787061eef44135e00fddb4b56b387a06c303bfd3884a6df9bea5cb730230/pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:138d31e3f90087f42aa6286fb640f3c7a8eb7bdae829418265e7e7474bd2574b", size = 2254716 }, - { url = "https://files.pythonhosted.org/packages/ae/e2/27262eb04963201e89f9c280f1e10c493a7a37bc877e023f31aa72d2f911/pydantic_core-2.33.0-cp313-cp313-win32.whl", hash = "sha256:d20cbb9d3e95114325780f3cfe990f3ecae24de7a2d75f978783878cce2ad585", size = 1916450 }, - { url = "https://files.pythonhosted.org/packages/13/8d/25ff96f1e89b19e0b70b3cd607c9ea7ca27e1dcb810a9cd4255ed6abf869/pydantic_core-2.33.0-cp313-cp313-win_amd64.whl", hash = "sha256:ca1103d70306489e3d006b0f79db8ca5dd3c977f6f13b2c59ff745249431a606", size = 1956092 }, - { url = "https://files.pythonhosted.org/packages/1b/64/66a2efeff657b04323ffcd7b898cb0354d36dae3a561049e092134a83e9c/pydantic_core-2.33.0-cp313-cp313-win_arm64.whl", hash = "sha256:6291797cad239285275558e0a27872da735b05c75d5237bbade8736f80e4c225", size = 1908367 }, - { url = "https://files.pythonhosted.org/packages/52/54/295e38769133363d7ec4a5863a4d579f331728c71a6644ff1024ee529315/pydantic_core-2.33.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7b79af799630af263eca9ec87db519426d8c9b3be35016eddad1832bac812d87", size = 1813331 }, - { url = "https://files.pythonhosted.org/packages/4c/9c/0c8ea02db8d682aa1ef48938abae833c1d69bdfa6e5ec13b21734b01ae70/pydantic_core-2.33.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eabf946a4739b5237f4f56d77fa6668263bc466d06a8036c055587c130a46f7b", size = 1986653 }, - { url = "https://files.pythonhosted.org/packages/8e/4f/3fb47d6cbc08c7e00f92300e64ba655428c05c56b8ab6723bd290bae6458/pydantic_core-2.33.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8a1d581e8cdbb857b0e0e81df98603376c1a5c34dc5e54039dcc00f043df81e7", size = 1931234 }, - { url = "https://files.pythonhosted.org/packages/32/b1/933e907c395a17c2ffa551112da2e6e725a200f951a91f61ae0b595a437d/pydantic_core-2.33.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:7c9c84749f5787781c1c45bb99f433402e484e515b40675a5d121ea14711cf61", size = 2043225 }, - { url = "https://files.pythonhosted.org/packages/05/92/86daeceaa2cf5e054fcc73e0fa17fe210aa004baf3d0530e4e0b4a0f08ce/pydantic_core-2.33.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:64672fa888595a959cfeff957a654e947e65bbe1d7d82f550417cbd6898a1d6b", size = 1877319 }, - { url = "https://files.pythonhosted.org/packages/20/c0/fab069cff6986c596a28af96f720ff84ec3ee5de6487f274e2b2f2d79c55/pydantic_core-2.33.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26bc7367c0961dec292244ef2549afa396e72e28cc24706210bd44d947582c59", size = 1910568 }, - { url = "https://files.pythonhosted.org/packages/6d/b5/c02cba6e0c661eb62eb1588a5775ba3e14d80f04071d684a8bd8ae1ca75b/pydantic_core-2.33.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ce72d46eb201ca43994303025bd54d8a35a3fc2a3495fac653d6eb7205ce04f4", size = 1997899 }, - { url = "https://files.pythonhosted.org/packages/cc/dc/96a4bb1ea6777e0329d609ade93cc3dca9bc71fd9cbe3f044c8ac39e7c24/pydantic_core-2.33.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14229c1504287533dbf6b1fc56f752ce2b4e9694022ae7509631ce346158de11", size = 2140646 }, - { url = "https://files.pythonhosted.org/packages/88/3d/9c8ce0dc418fa9b10bc994449ca6d251493525a6debc5f73b07a367b3ced/pydantic_core-2.33.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:085d8985b1c1e48ef271e98a658f562f29d89bda98bf120502283efbc87313eb", size = 2753924 }, - { url = "https://files.pythonhosted.org/packages/17/d6/a9cee7d4689d51bfd01107c2ec8de394f56e974ea4ae7e2d624712bed67a/pydantic_core-2.33.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31860fbda80d8f6828e84b4a4d129fd9c4535996b8249cfb8c720dc2a1a00bb8", size = 2008316 }, - { url = "https://files.pythonhosted.org/packages/d5/ea/c2578b67b28f3e51323841632e217a5fdd0a8f3fce852bb16782e637cda7/pydantic_core-2.33.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f200b2f20856b5a6c3a35f0d4e344019f805e363416e609e9b47c552d35fd5ea", size = 2124634 }, - { url = "https://files.pythonhosted.org/packages/1f/ae/236dbc8085a88aec1fd8369c6062fff3b40463918af90d20a2058b967f0e/pydantic_core-2.33.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f72914cfd1d0176e58ddc05c7a47674ef4222c8253bf70322923e73e14a4ac3", size = 2087826 }, - { url = "https://files.pythonhosted.org/packages/12/ad/8292aebcd787b03167a62df5221e613b76b263b5a05c2310217e88772b75/pydantic_core-2.33.0-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:91301a0980a1d4530d4ba7e6a739ca1a6b31341252cb709948e0aca0860ce0ae", size = 2260866 }, - { url = "https://files.pythonhosted.org/packages/83/f9/d89c9e306f69395fb5b0d6e83e99980046c2b3a7cc2839a43b869838bf60/pydantic_core-2.33.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7419241e17c7fbe5074ba79143d5523270e04f86f1b3a0dff8df490f84c8273a", size = 2259118 }, - { url = "https://files.pythonhosted.org/packages/30/f1/4da918dcd75898006a6b4da848f231306a2d8b2fda35c7679df76a4ae3d7/pydantic_core-2.33.0-cp39-cp39-win32.whl", hash = "sha256:7a25493320203005d2a4dac76d1b7d953cb49bce6d459d9ae38e30dd9f29bc9c", size = 1925241 }, - { url = "https://files.pythonhosted.org/packages/4f/53/a31aaa220ac133f05e4e3622f65ad9b02e6cbd89723d8d035f5effac8701/pydantic_core-2.33.0-cp39-cp39-win_amd64.whl", hash = "sha256:82a4eba92b7ca8af1b7d5ef5f3d9647eee94d1f74d21ca7c21e3a2b92e008358", size = 1953427 }, - { url = "https://files.pythonhosted.org/packages/44/77/85e173b715e1a277ce934f28d877d82492df13e564fa68a01c96f36a47ad/pydantic_core-2.33.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e2762c568596332fdab56b07060c8ab8362c56cf2a339ee54e491cd503612c50", size = 2040129 }, - { url = "https://files.pythonhosted.org/packages/33/e7/33da5f8a94bbe2191cfcd15bd6d16ecd113e67da1b8c78d3cc3478112dab/pydantic_core-2.33.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5bf637300ff35d4f59c006fff201c510b2b5e745b07125458a5389af3c0dff8c", size = 1872656 }, - { url = "https://files.pythonhosted.org/packages/b4/7a/9600f222bea840e5b9ba1f17c0acc79b669b24542a78c42c6a10712c0aae/pydantic_core-2.33.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c151ce3d59ed56ebd7ce9ce5986a409a85db697d25fc232f8e81f195aa39a1", size = 1903731 }, - { url = "https://files.pythonhosted.org/packages/81/d2/94c7ca4e24c5dcfb74df92e0836c189e9eb6814cf62d2f26a75ea0a906db/pydantic_core-2.33.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee65f0cc652261744fd07f2c6e6901c914aa6c5ff4dcfaf1136bc394d0dd26b", size = 2083966 }, - { url = "https://files.pythonhosted.org/packages/b8/74/a0259989d220e8865ed6866a6d40539e40fa8f507e587e35d2414cc081f8/pydantic_core-2.33.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:024d136ae44d233e6322027bbf356712b3940bee816e6c948ce4b90f18471b3d", size = 2118951 }, - { url = "https://files.pythonhosted.org/packages/13/4c/87405ed04d6d07597920b657f082a8e8e58bf3034178bb9044b4d57a91e2/pydantic_core-2.33.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e37f10f6d4bc67c58fbd727108ae1d8b92b397355e68519f1e4a7babb1473442", size = 2079632 }, - { url = "https://files.pythonhosted.org/packages/5a/4c/bcb02970ef91d4cd6de7c6893101302637da456bc8b52c18ea0d047b55ce/pydantic_core-2.33.0-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:502ed542e0d958bd12e7c3e9a015bce57deaf50eaa8c2e1c439b512cb9db1e3a", size = 2250541 }, - { url = "https://files.pythonhosted.org/packages/a3/2b/dbe5450c4cd904be5da736dcc7f2357b828199e29e38de19fc81f988b288/pydantic_core-2.33.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:715c62af74c236bf386825c0fdfa08d092ab0f191eb5b4580d11c3189af9d330", size = 2255685 }, - { url = "https://files.pythonhosted.org/packages/ca/a6/ca1d35f695d81f639c5617fc9efb44caad21a9463383fa45364b3044175a/pydantic_core-2.33.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bccc06fa0372151f37f6b69834181aa9eb57cf8665ed36405fb45fbf6cac3bae", size = 2082395 }, - { url = "https://files.pythonhosted.org/packages/2b/b2/553e42762e7b08771fca41c0230c1ac276f9e79e78f57628e1b7d328551d/pydantic_core-2.33.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5d8dc9f63a26f7259b57f46a7aab5af86b2ad6fbe48487500bb1f4b27e051e4c", size = 2041207 }, - { url = "https://files.pythonhosted.org/packages/85/81/a91a57bbf3efe53525ab75f65944b8950e6ef84fe3b9a26c1ec173363263/pydantic_core-2.33.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30369e54d6d0113d2aa5aee7a90d17f225c13d87902ace8fcd7bbf99b19124db", size = 1873736 }, - { url = "https://files.pythonhosted.org/packages/9c/d2/5ab52e9f551cdcbc1ee99a0b3ef595f56d031f66f88e5ca6726c49f9ce65/pydantic_core-2.33.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3eb479354c62067afa62f53bb387827bee2f75c9c79ef25eef6ab84d4b1ae3b", size = 1903794 }, - { url = "https://files.pythonhosted.org/packages/2f/5f/a81742d3f3821b16f1265f057d6e0b68a3ab13a814fe4bffac536a1f26fd/pydantic_core-2.33.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0310524c833d91403c960b8a3cf9f46c282eadd6afd276c8c5edc617bd705dc9", size = 2083457 }, - { url = "https://files.pythonhosted.org/packages/b5/2f/e872005bc0fc47f9c036b67b12349a8522d32e3bda928e82d676e2a594d1/pydantic_core-2.33.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eddb18a00bbb855325db27b4c2a89a4ba491cd6a0bd6d852b225172a1f54b36c", size = 2119537 }, - { url = "https://files.pythonhosted.org/packages/d3/13/183f13ce647202eaf3dada9e42cdfc59cbb95faedd44d25f22b931115c7f/pydantic_core-2.33.0-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ade5dbcf8d9ef8f4b28e682d0b29f3008df9842bb5ac48ac2c17bc55771cc976", size = 2080069 }, - { url = "https://files.pythonhosted.org/packages/23/8b/b6be91243da44a26558d9c3a9007043b3750334136c6550551e8092d6d96/pydantic_core-2.33.0-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2c0afd34f928383e3fd25740f2050dbac9d077e7ba5adbaa2227f4d4f3c8da5c", size = 2251618 }, - { url = "https://files.pythonhosted.org/packages/aa/c5/fbcf1977035b834f63eb542e74cd6c807177f383386175b468f0865bcac4/pydantic_core-2.33.0-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:7da333f21cd9df51d5731513a6d39319892947604924ddf2e24a4612975fb936", size = 2255374 }, - { url = "https://files.pythonhosted.org/packages/2f/f8/66f328e411f1c9574b13c2c28ab01f308b53688bbbe6ca8fb981e6cabc42/pydantic_core-2.33.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4b6d77c75a57f041c5ee915ff0b0bb58eabb78728b69ed967bc5b780e8f701b8", size = 2082099 }, - { url = "https://files.pythonhosted.org/packages/a7/b2/7d0182cb46cfa1e003a5a52b6a15d50ad3c191a34ca5e6f5726a56ac016f/pydantic_core-2.33.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ba95691cf25f63df53c1d342413b41bd7762d9acb425df8858d7efa616c0870e", size = 2040349 }, - { url = "https://files.pythonhosted.org/packages/58/9f/dc18700d82cd4e053ff02155d40cff89b08d8583668a0b54ca1b223d3132/pydantic_core-2.33.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:4f1ab031feb8676f6bd7c85abec86e2935850bf19b84432c64e3e239bffeb1ec", size = 1873052 }, - { url = "https://files.pythonhosted.org/packages/06/a9/a30a2603121b5841dc2b8dea4e18db74fa83c8c9d4804401dec23bcd3bb0/pydantic_core-2.33.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58c1151827eef98b83d49b6ca6065575876a02d2211f259fb1a6b7757bd24dd8", size = 1904205 }, - { url = "https://files.pythonhosted.org/packages/53/b7/cc7638fd83ad8bb19cab297e3f0a669bd9633830833865c064a74ff5a1c1/pydantic_core-2.33.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a66d931ea2c1464b738ace44b7334ab32a2fd50be023d863935eb00f42be1778", size = 2084567 }, - { url = "https://files.pythonhosted.org/packages/c4/f0/37ba8bdc15d2c233b2a3675160cc1b205e30dd9ef4cd6d3dfe069799e160/pydantic_core-2.33.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0bcf0bab28995d483f6c8d7db25e0d05c3efa5cebfd7f56474359e7137f39856", size = 2119072 }, - { url = "https://files.pythonhosted.org/packages/eb/29/e553e2e9c16e5ad9370e947f15585db4f7438ab4b52c53f93695c99831cd/pydantic_core-2.33.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:89670d7a0045acb52be0566df5bc8b114ac967c662c06cf5e0c606e4aadc964b", size = 2080432 }, - { url = "https://files.pythonhosted.org/packages/65/ca/268cae039ea91366ba88b9a848977b7189cb7675cb2cd9ee273464a20d91/pydantic_core-2.33.0-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:b716294e721d8060908dbebe32639b01bfe61b15f9f57bcc18ca9a0e00d9520b", size = 2251007 }, - { url = "https://files.pythonhosted.org/packages/3c/a4/5ca3a14b5d992e63a766b8883d4ba8b4d353ef6a2d9f59ee5d60e728998a/pydantic_core-2.33.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fc53e05c16697ff0c1c7c2b98e45e131d4bfb78068fffff92a82d169cbb4c7b7", size = 2256435 }, - { url = "https://files.pythonhosted.org/packages/da/a2/2670964d7046025b96f8c6d35c38e5310ec6aa1681e4158ef31ab21a4727/pydantic_core-2.33.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:68504959253303d3ae9406b634997a2123a0b0c1da86459abbd0ffc921695eac", size = 2082790 }, +sdist = { url = "https://files.pythonhosted.org/packages/17/19/ed6a078a5287aea7922de6841ef4c06157931622c89c2a47940837b5eecd/pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df", size = 434395 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/ea/5f572806ab4d4223d11551af814d243b0e3e02cc6913def4d1fe4a5ca41c/pydantic_core-2.33.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3077cfdb6125cc8dab61b155fdd714663e401f0e6883f9632118ec12cf42df26", size = 2044021 }, + { url = "https://files.pythonhosted.org/packages/8c/d1/f86cc96d2aa80e3881140d16d12ef2b491223f90b28b9a911346c04ac359/pydantic_core-2.33.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ffab8b2908d152e74862d276cf5017c81a2f3719f14e8e3e8d6b83fda863927", size = 1861742 }, + { url = "https://files.pythonhosted.org/packages/37/08/fbd2cd1e9fc735a0df0142fac41c114ad9602d1c004aea340169ae90973b/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5183e4f6a2d468787243ebcd70cf4098c247e60d73fb7d68d5bc1e1beaa0c4db", size = 1910414 }, + { url = "https://files.pythonhosted.org/packages/7f/73/3ac217751decbf8d6cb9443cec9b9eb0130eeada6ae56403e11b486e277e/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:398a38d323f37714023be1e0285765f0a27243a8b1506b7b7de87b647b517e48", size = 1996848 }, + { url = "https://files.pythonhosted.org/packages/9a/f5/5c26b265cdcff2661e2520d2d1e9db72d117ea00eb41e00a76efe68cb009/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87d3776f0001b43acebfa86f8c64019c043b55cc5a6a2e313d728b5c95b46969", size = 2141055 }, + { url = "https://files.pythonhosted.org/packages/5d/14/a9c3cee817ef2f8347c5ce0713e91867a0dceceefcb2973942855c917379/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c566dd9c5f63d22226409553531f89de0cac55397f2ab8d97d6f06cfce6d947e", size = 2753806 }, + { url = "https://files.pythonhosted.org/packages/f2/68/866ce83a51dd37e7c604ce0050ff6ad26de65a7799df89f4db87dd93d1d6/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0d5f3acc81452c56895e90643a625302bd6be351e7010664151cc55b7b97f89", size = 2007777 }, + { url = "https://files.pythonhosted.org/packages/b6/a8/36771f4404bb3e49bd6d4344da4dede0bf89cc1e01f3b723c47248a3761c/pydantic_core-2.33.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3a07fadec2a13274a8d861d3d37c61e97a816beae717efccaa4b36dfcaadcde", size = 2122803 }, + { url = "https://files.pythonhosted.org/packages/18/9c/730a09b2694aa89360d20756369822d98dc2f31b717c21df33b64ffd1f50/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f99aeda58dce827f76963ee87a0ebe75e648c72ff9ba1174a253f6744f518f65", size = 2086755 }, + { url = "https://files.pythonhosted.org/packages/54/8e/2dccd89602b5ec31d1c58138d02340ecb2ebb8c2cac3cc66b65ce3edb6ce/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:902dbc832141aa0ec374f4310f1e4e7febeebc3256f00dc359a9ac3f264a45dc", size = 2257358 }, + { url = "https://files.pythonhosted.org/packages/d1/9c/126e4ac1bfad8a95a9837acdd0963695d69264179ba4ede8b8c40d741702/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fe44d56aa0b00d66640aa84a3cbe80b7a3ccdc6f0b1ca71090696a6d4777c091", size = 2257916 }, + { url = "https://files.pythonhosted.org/packages/7d/ba/91eea2047e681a6853c81c20aeca9dcdaa5402ccb7404a2097c2adf9d038/pydantic_core-2.33.1-cp310-cp310-win32.whl", hash = "sha256:ed3eb16d51257c763539bde21e011092f127a2202692afaeaccb50db55a31383", size = 1923823 }, + { url = "https://files.pythonhosted.org/packages/94/c0/fcdf739bf60d836a38811476f6ecd50374880b01e3014318b6e809ddfd52/pydantic_core-2.33.1-cp310-cp310-win_amd64.whl", hash = "sha256:694ad99a7f6718c1a498dc170ca430687a39894a60327f548e02a9c7ee4b6504", size = 1952494 }, + { url = "https://files.pythonhosted.org/packages/d6/7f/c6298830cb780c46b4f46bb24298d01019ffa4d21769f39b908cd14bbd50/pydantic_core-2.33.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e966fc3caaf9f1d96b349b0341c70c8d6573bf1bac7261f7b0ba88f96c56c24", size = 2044224 }, + { url = "https://files.pythonhosted.org/packages/a8/65/6ab3a536776cad5343f625245bd38165d6663256ad43f3a200e5936afd6c/pydantic_core-2.33.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bfd0adeee563d59c598ceabddf2c92eec77abcb3f4a391b19aa7366170bd9e30", size = 1858845 }, + { url = "https://files.pythonhosted.org/packages/e9/15/9a22fd26ba5ee8c669d4b8c9c244238e940cd5d818649603ca81d1c69861/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91815221101ad3c6b507804178a7bb5cb7b2ead9ecd600041669c8d805ebd595", size = 1910029 }, + { url = "https://files.pythonhosted.org/packages/d5/33/8cb1a62818974045086f55f604044bf35b9342900318f9a2a029a1bec460/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9fea9c1869bb4742d174a57b4700c6dadea951df8b06de40c2fedb4f02931c2e", size = 1997784 }, + { url = "https://files.pythonhosted.org/packages/c0/ca/49958e4df7715c71773e1ea5be1c74544923d10319173264e6db122543f9/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d20eb4861329bb2484c021b9d9a977566ab16d84000a57e28061151c62b349a", size = 2141075 }, + { url = "https://files.pythonhosted.org/packages/7b/a6/0b3a167a9773c79ba834b959b4e18c3ae9216b8319bd8422792abc8a41b1/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb935c5591573ae3201640579f30128ccc10739b45663f93c06796854405505", size = 2745849 }, + { url = "https://files.pythonhosted.org/packages/0b/60/516484135173aa9e5861d7a0663dce82e4746d2e7f803627d8c25dfa5578/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c964fd24e6166420d18fb53996d8c9fd6eac9bf5ae3ec3d03015be4414ce497f", size = 2005794 }, + { url = "https://files.pythonhosted.org/packages/86/70/05b1eb77459ad47de00cf78ee003016da0cedf8b9170260488d7c21e9181/pydantic_core-2.33.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:681d65e9011f7392db5aa002b7423cc442d6a673c635668c227c6c8d0e5a4f77", size = 2123237 }, + { url = "https://files.pythonhosted.org/packages/c7/57/12667a1409c04ae7dc95d3b43158948eb0368e9c790be8b095cb60611459/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e100c52f7355a48413e2999bfb4e139d2977a904495441b374f3d4fb4a170961", size = 2086351 }, + { url = "https://files.pythonhosted.org/packages/57/61/cc6d1d1c1664b58fdd6ecc64c84366c34ec9b606aeb66cafab6f4088974c/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:048831bd363490be79acdd3232f74a0e9951b11b2b4cc058aeb72b22fdc3abe1", size = 2258914 }, + { url = "https://files.pythonhosted.org/packages/d1/0a/edb137176a1f5419b2ddee8bde6a0a548cfa3c74f657f63e56232df8de88/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bdc84017d28459c00db6f918a7272a5190bec3090058334e43a76afb279eac7c", size = 2257385 }, + { url = "https://files.pythonhosted.org/packages/26/3c/48ca982d50e4b0e1d9954919c887bdc1c2b462801bf408613ccc641b3daa/pydantic_core-2.33.1-cp311-cp311-win32.whl", hash = "sha256:32cd11c5914d1179df70406427097c7dcde19fddf1418c787540f4b730289896", size = 1923765 }, + { url = "https://files.pythonhosted.org/packages/33/cd/7ab70b99e5e21559f5de38a0928ea84e6f23fdef2b0d16a6feaf942b003c/pydantic_core-2.33.1-cp311-cp311-win_amd64.whl", hash = "sha256:2ea62419ba8c397e7da28a9170a16219d310d2cf4970dbc65c32faf20d828c83", size = 1950688 }, + { url = "https://files.pythonhosted.org/packages/4b/ae/db1fc237b82e2cacd379f63e3335748ab88b5adde98bf7544a1b1bd10a84/pydantic_core-2.33.1-cp311-cp311-win_arm64.whl", hash = "sha256:fc903512177361e868bc1f5b80ac8c8a6e05fcdd574a5fb5ffeac5a9982b9e89", size = 1908185 }, + { url = "https://files.pythonhosted.org/packages/c8/ce/3cb22b07c29938f97ff5f5bb27521f95e2ebec399b882392deb68d6c440e/pydantic_core-2.33.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1293d7febb995e9d3ec3ea09caf1a26214eec45b0f29f6074abb004723fc1de8", size = 2026640 }, + { url = "https://files.pythonhosted.org/packages/19/78/f381d643b12378fee782a72126ec5d793081ef03791c28a0fd542a5bee64/pydantic_core-2.33.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99b56acd433386c8f20be5c4000786d1e7ca0523c8eefc995d14d79c7a081498", size = 1852649 }, + { url = "https://files.pythonhosted.org/packages/9d/2b/98a37b80b15aac9eb2c6cfc6dbd35e5058a352891c5cce3a8472d77665a6/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a5ec3fa8c2fe6c53e1b2ccc2454398f95d5393ab398478f53e1afbbeb4d939", size = 1892472 }, + { url = "https://files.pythonhosted.org/packages/4e/d4/3c59514e0f55a161004792b9ff3039da52448f43f5834f905abef9db6e4a/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b172f7b9d2f3abc0efd12e3386f7e48b576ef309544ac3a63e5e9cdd2e24585d", size = 1977509 }, + { url = "https://files.pythonhosted.org/packages/a9/b6/c2c7946ef70576f79a25db59a576bce088bdc5952d1b93c9789b091df716/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9097b9f17f91eea659b9ec58148c0747ec354a42f7389b9d50701610d86f812e", size = 2128702 }, + { url = "https://files.pythonhosted.org/packages/88/fe/65a880f81e3f2a974312b61f82a03d85528f89a010ce21ad92f109d94deb/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc77ec5b7e2118b152b0d886c7514a4653bcb58c6b1d760134a9fab915f777b3", size = 2679428 }, + { url = "https://files.pythonhosted.org/packages/6f/ff/4459e4146afd0462fb483bb98aa2436d69c484737feaceba1341615fb0ac/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3d15245b08fa4a84cefc6c9222e6f37c98111c8679fbd94aa145f9a0ae23d", size = 2008753 }, + { url = "https://files.pythonhosted.org/packages/7c/76/1c42e384e8d78452ededac8b583fe2550c84abfef83a0552e0e7478ccbc3/pydantic_core-2.33.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef99779001d7ac2e2461d8ab55d3373fe7315caefdbecd8ced75304ae5a6fc6b", size = 2114849 }, + { url = "https://files.pythonhosted.org/packages/00/72/7d0cf05095c15f7ffe0eb78914b166d591c0eed72f294da68378da205101/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fc6bf8869e193855e8d91d91f6bf59699a5cdfaa47a404e278e776dd7f168b39", size = 2069541 }, + { url = "https://files.pythonhosted.org/packages/b3/69/94a514066bb7d8be499aa764926937409d2389c09be0b5107a970286ef81/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:b1caa0bc2741b043db7823843e1bde8aaa58a55a58fda06083b0569f8b45693a", size = 2239225 }, + { url = "https://files.pythonhosted.org/packages/84/b0/e390071eadb44b41f4f54c3cef64d8bf5f9612c92686c9299eaa09e267e2/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ec259f62538e8bf364903a7d0d0239447059f9434b284f5536e8402b7dd198db", size = 2248373 }, + { url = "https://files.pythonhosted.org/packages/d6/b2/288b3579ffc07e92af66e2f1a11be3b056fe1214aab314748461f21a31c3/pydantic_core-2.33.1-cp312-cp312-win32.whl", hash = "sha256:e14f369c98a7c15772b9da98987f58e2b509a93235582838bd0d1d8c08b68fda", size = 1907034 }, + { url = "https://files.pythonhosted.org/packages/02/28/58442ad1c22b5b6742b992ba9518420235adced665513868f99a1c2638a5/pydantic_core-2.33.1-cp312-cp312-win_amd64.whl", hash = "sha256:1c607801d85e2e123357b3893f82c97a42856192997b95b4d8325deb1cd0c5f4", size = 1956848 }, + { url = "https://files.pythonhosted.org/packages/a1/eb/f54809b51c7e2a1d9f439f158b8dd94359321abcc98767e16fc48ae5a77e/pydantic_core-2.33.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d13f0276806ee722e70a1c93da19748594f19ac4299c7e41237fc791d1861ea", size = 1903986 }, + { url = "https://files.pythonhosted.org/packages/7a/24/eed3466a4308d79155f1cdd5c7432c80ddcc4530ba8623b79d5ced021641/pydantic_core-2.33.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70af6a21237b53d1fe7b9325b20e65cbf2f0a848cf77bed492b029139701e66a", size = 2033551 }, + { url = "https://files.pythonhosted.org/packages/ab/14/df54b1a0bc9b6ded9b758b73139d2c11b4e8eb43e8ab9c5847c0a2913ada/pydantic_core-2.33.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:282b3fe1bbbe5ae35224a0dbd05aed9ccabccd241e8e6b60370484234b456266", size = 1852785 }, + { url = "https://files.pythonhosted.org/packages/fa/96/e275f15ff3d34bb04b0125d9bc8848bf69f25d784d92a63676112451bfb9/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b315e596282bbb5822d0c7ee9d255595bd7506d1cb20c2911a4da0b970187d3", size = 1897758 }, + { url = "https://files.pythonhosted.org/packages/b7/d8/96bc536e975b69e3a924b507d2a19aedbf50b24e08c80fb00e35f9baaed8/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dfae24cf9921875ca0ca6a8ecb4bb2f13c855794ed0d468d6abbec6e6dcd44a", size = 1986109 }, + { url = "https://files.pythonhosted.org/packages/90/72/ab58e43ce7e900b88cb571ed057b2fcd0e95b708a2e0bed475b10130393e/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dd8ecfde08d8bfadaea669e83c63939af76f4cf5538a72597016edfa3fad516", size = 2129159 }, + { url = "https://files.pythonhosted.org/packages/dc/3f/52d85781406886c6870ac995ec0ba7ccc028b530b0798c9080531b409fdb/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f593494876eae852dc98c43c6f260f45abdbfeec9e4324e31a481d948214764", size = 2680222 }, + { url = "https://files.pythonhosted.org/packages/f4/56/6e2ef42f363a0eec0fd92f74a91e0ac48cd2e49b695aac1509ad81eee86a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948b73114f47fd7016088e5186d13faf5e1b2fe83f5e320e371f035557fd264d", size = 2006980 }, + { url = "https://files.pythonhosted.org/packages/4c/c0/604536c4379cc78359f9ee0aa319f4aedf6b652ec2854953f5a14fc38c5a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11f3864eb516af21b01e25fac915a82e9ddad3bb0fb9e95a246067398b435a4", size = 2120840 }, + { url = "https://files.pythonhosted.org/packages/1f/46/9eb764814f508f0edfb291a0f75d10854d78113fa13900ce13729aaec3ae/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:549150be302428b56fdad0c23c2741dcdb5572413776826c965619a25d9c6bde", size = 2072518 }, + { url = "https://files.pythonhosted.org/packages/42/e3/fb6b2a732b82d1666fa6bf53e3627867ea3131c5f39f98ce92141e3e3dc1/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:495bc156026efafd9ef2d82372bd38afce78ddd82bf28ef5276c469e57c0c83e", size = 2248025 }, + { url = "https://files.pythonhosted.org/packages/5c/9d/fbe8fe9d1aa4dac88723f10a921bc7418bd3378a567cb5e21193a3c48b43/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ec79de2a8680b1a67a07490bddf9636d5c2fab609ba8c57597e855fa5fa4dacd", size = 2254991 }, + { url = "https://files.pythonhosted.org/packages/aa/99/07e2237b8a66438d9b26482332cda99a9acccb58d284af7bc7c946a42fd3/pydantic_core-2.33.1-cp313-cp313-win32.whl", hash = "sha256:ee12a7be1742f81b8a65b36c6921022301d466b82d80315d215c4c691724986f", size = 1915262 }, + { url = "https://files.pythonhosted.org/packages/8a/f4/e457a7849beeed1e5defbcf5051c6f7b3c91a0624dd31543a64fc9adcf52/pydantic_core-2.33.1-cp313-cp313-win_amd64.whl", hash = "sha256:ede9b407e39949d2afc46385ce6bd6e11588660c26f80576c11c958e6647bc40", size = 1956626 }, + { url = "https://files.pythonhosted.org/packages/20/d0/e8d567a7cff7b04e017ae164d98011f1e1894269fe8e90ea187a3cbfb562/pydantic_core-2.33.1-cp313-cp313-win_arm64.whl", hash = "sha256:aa687a23d4b7871a00e03ca96a09cad0f28f443690d300500603bd0adba4b523", size = 1909590 }, + { url = "https://files.pythonhosted.org/packages/ef/fd/24ea4302d7a527d672c5be06e17df16aabfb4e9fdc6e0b345c21580f3d2a/pydantic_core-2.33.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:401d7b76e1000d0dd5538e6381d28febdcacb097c8d340dde7d7fc6e13e9f95d", size = 1812963 }, + { url = "https://files.pythonhosted.org/packages/5f/95/4fbc2ecdeb5c1c53f1175a32d870250194eb2fdf6291b795ab08c8646d5d/pydantic_core-2.33.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aeb055a42d734c0255c9e489ac67e75397d59c6fbe60d155851e9782f276a9c", size = 1986896 }, + { url = "https://files.pythonhosted.org/packages/71/ae/fe31e7f4a62431222d8f65a3bd02e3fa7e6026d154a00818e6d30520ea77/pydantic_core-2.33.1-cp313-cp313t-win_amd64.whl", hash = "sha256:338ea9b73e6e109f15ab439e62cb3b78aa752c7fd9536794112e14bee02c8d18", size = 1931810 }, + { url = "https://files.pythonhosted.org/packages/49/78/b86bad645cc3e8dfa6858c70ec38939bf350e54004837c48de09474b2b9e/pydantic_core-2.33.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:5ab77f45d33d264de66e1884fca158bc920cb5e27fd0764a72f72f5756ae8bdb", size = 2044282 }, + { url = "https://files.pythonhosted.org/packages/3b/00/a02531331773b2bf08743d84c6b776bd6a449d23b3ae6b0e3229d568bac4/pydantic_core-2.33.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7aaba1b4b03aaea7bb59e1b5856d734be011d3e6d98f5bcaa98cb30f375f2ad", size = 1877598 }, + { url = "https://files.pythonhosted.org/packages/a1/fa/32cc152b84a1f420f8a7d80161373e8d87d4ffa077e67d6c8aab3ce1a6ab/pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fb66263e9ba8fea2aa85e1e5578980d127fb37d7f2e292773e7bc3a38fb0c7b", size = 1911021 }, + { url = "https://files.pythonhosted.org/packages/5e/87/ea553e0d98bce6c4876f8c50f65cb45597eff6e0aaa8b15813e9972bb19d/pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3f2648b9262607a7fb41d782cc263b48032ff7a03a835581abbf7a3bec62bcf5", size = 1997276 }, + { url = "https://files.pythonhosted.org/packages/f7/9b/60cb9f4b52158b3adac0066492bbadd0b8473f4f8da5bcc73972655b76ef/pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:723c5630c4259400818b4ad096735a829074601805d07f8cafc366d95786d331", size = 2141348 }, + { url = "https://files.pythonhosted.org/packages/9b/38/374d254e270d4de0add68a8239f4ed0f444fdd7b766ea69244fb9491dccb/pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d100e3ae783d2167782391e0c1c7a20a31f55f8015f3293647544df3f9c67824", size = 2753708 }, + { url = "https://files.pythonhosted.org/packages/05/a8/fd79111eb5ab9bc4ef98d8fb0b3a2ffdc80107b2c59859a741ab379c96f8/pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177d50460bc976a0369920b6c744d927b0ecb8606fb56858ff542560251b19e5", size = 2008699 }, + { url = "https://files.pythonhosted.org/packages/35/31/2e06619868eb4c18642c5601db420599c1cf9cf50fe868c9ac09cd298e24/pydantic_core-2.33.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3edde68d1a1f9af1273b2fe798997b33f90308fb6d44d8550c89fc6a3647cf6", size = 2123426 }, + { url = "https://files.pythonhosted.org/packages/4a/d0/3531e8783a311802e3db7ee5a1a5ed79e5706e930b1b4e3109ce15eeb681/pydantic_core-2.33.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a62c3c3ef6a7e2c45f7853b10b5bc4ddefd6ee3cd31024754a1a5842da7d598d", size = 2087330 }, + { url = "https://files.pythonhosted.org/packages/ac/32/5ff252ed73bacd7677a706ab17723e261a76793f98b305aa20cfc10bbd56/pydantic_core-2.33.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:c91dbb0ab683fa0cd64a6e81907c8ff41d6497c346890e26b23de7ee55353f96", size = 2258171 }, + { url = "https://files.pythonhosted.org/packages/c9/f9/e96e00f92b8f5b3e2cddc80c5ee6cf038f8a0f238c44b67b01759943a7b4/pydantic_core-2.33.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f466e8bf0a62dc43e068c12166281c2eca72121dd2adc1040f3aa1e21ef8599", size = 2258745 }, + { url = "https://files.pythonhosted.org/packages/54/1e/51c86688e809d94797fdf0efc41514f001caec982a05f62d90c180a9639d/pydantic_core-2.33.1-cp39-cp39-win32.whl", hash = "sha256:ab0277cedb698749caada82e5d099dc9fed3f906a30d4c382d1a21725777a1e5", size = 1923626 }, + { url = "https://files.pythonhosted.org/packages/57/18/c2da959fd8d019b70cadafdda2bf845378ada47973e0bad6cc84f56dbe6e/pydantic_core-2.33.1-cp39-cp39-win_amd64.whl", hash = "sha256:5773da0ee2d17136b1f1c6fbde543398d452a6ad2a7b54ea1033e2daa739b8d2", size = 1953703 }, + { url = "https://files.pythonhosted.org/packages/9c/c7/8b311d5adb0fe00a93ee9b4e92a02b0ec08510e9838885ef781ccbb20604/pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c834f54f8f4640fd7e4b193f80eb25a0602bba9e19b3cd2fc7ffe8199f5ae02", size = 2041659 }, + { url = "https://files.pythonhosted.org/packages/8a/d6/4f58d32066a9e26530daaf9adc6664b01875ae0691570094968aaa7b8fcc/pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:049e0de24cf23766f12cc5cc71d8abc07d4a9deb9061b334b62093dedc7cb068", size = 1873294 }, + { url = "https://files.pythonhosted.org/packages/f7/3f/53cc9c45d9229da427909c751f8ed2bf422414f7664ea4dde2d004f596ba/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a28239037b3d6f16916a4c831a5a0eadf856bdd6d2e92c10a0da3a59eadcf3e", size = 1903771 }, + { url = "https://files.pythonhosted.org/packages/f0/49/bf0783279ce674eb9903fb9ae43f6c614cb2f1c4951370258823f795368b/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d3da303ab5f378a268fa7d45f37d7d85c3ec19769f28d2cc0c61826a8de21fe", size = 2083558 }, + { url = "https://files.pythonhosted.org/packages/9c/5b/0d998367687f986c7d8484a2c476d30f07bf5b8b1477649a6092bd4c540e/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25626fb37b3c543818c14821afe0fd3830bc327a43953bc88db924b68c5723f1", size = 2118038 }, + { url = "https://files.pythonhosted.org/packages/b3/33/039287d410230ee125daee57373ac01940d3030d18dba1c29cd3089dc3ca/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3ab2d36e20fbfcce8f02d73c33a8a7362980cff717926bbae030b93ae46b56c7", size = 2079315 }, + { url = "https://files.pythonhosted.org/packages/1f/85/6d8b2646d99c062d7da2d0ab2faeb0d6ca9cca4c02da6076376042a20da3/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2f9284e11c751b003fd4215ad92d325d92c9cb19ee6729ebd87e3250072cdcde", size = 2249063 }, + { url = "https://files.pythonhosted.org/packages/17/d7/c37d208d5738f7b9ad8f22ae8a727d88ebf9c16c04ed2475122cc3f7224a/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:048c01eee07d37cbd066fc512b9d8b5ea88ceeb4e629ab94b3e56965ad655add", size = 2254631 }, + { url = "https://files.pythonhosted.org/packages/13/e0/bafa46476d328e4553b85ab9b2f7409e7aaef0ce4c937c894821c542d347/pydantic_core-2.33.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5ccd429694cf26af7997595d627dd2637e7932214486f55b8a357edaac9dae8c", size = 2080877 }, + { url = "https://files.pythonhosted.org/packages/0b/76/1794e440c1801ed35415238d2c728f26cd12695df9057154ad768b7b991c/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a371dc00282c4b84246509a5ddc808e61b9864aa1eae9ecc92bb1268b82db4a", size = 2042858 }, + { url = "https://files.pythonhosted.org/packages/73/b4/9cd7b081fb0b1b4f8150507cd59d27b275c3e22ad60b35cb19ea0977d9b9/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f59295ecc75a1788af8ba92f2e8c6eeaa5a94c22fc4d151e8d9638814f85c8fc", size = 1873745 }, + { url = "https://files.pythonhosted.org/packages/e1/d7/9ddb7575d4321e40d0363903c2576c8c0c3280ebea137777e5ab58d723e3/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08530b8ac922003033f399128505f513e30ca770527cc8bbacf75a84fcc2c74b", size = 1904188 }, + { url = "https://files.pythonhosted.org/packages/d1/a8/3194ccfe461bb08da19377ebec8cb4f13c9bd82e13baebc53c5c7c39a029/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae370459da6a5466978c0eacf90690cb57ec9d533f8e63e564ef3822bfa04fe", size = 2083479 }, + { url = "https://files.pythonhosted.org/packages/42/c7/84cb569555d7179ca0b3f838cef08f66f7089b54432f5b8599aac6e9533e/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e3de2777e3b9f4d603112f78006f4ae0acb936e95f06da6cb1a45fbad6bdb4b5", size = 2118415 }, + { url = "https://files.pythonhosted.org/packages/3b/67/72abb8c73e0837716afbb58a59cc9e3ae43d1aa8677f3b4bc72c16142716/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a64e81e8cba118e108d7126362ea30e021291b7805d47e4896e52c791be2761", size = 2079623 }, + { url = "https://files.pythonhosted.org/packages/0b/cd/c59707e35a47ba4cbbf153c3f7c56420c58653b5801b055dc52cccc8e2dc/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:52928d8c1b6bda03cc6d811e8923dffc87a2d3c8b3bfd2ce16471c7147a24850", size = 2250175 }, + { url = "https://files.pythonhosted.org/packages/84/32/e4325a6676b0bed32d5b084566ec86ed7fd1e9bcbfc49c578b1755bde920/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1b30d92c9412beb5ac6b10a3eb7ef92ccb14e3f2a8d7732e2d739f58b3aa7544", size = 2254674 }, + { url = "https://files.pythonhosted.org/packages/12/6f/5596dc418f2e292ffc661d21931ab34591952e2843e7168ea5a52591f6ff/pydantic_core-2.33.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f995719707e0e29f0f41a8aa3bcea6e761a36c9136104d3189eafb83f5cec5e5", size = 2080951 }, + { url = "https://files.pythonhosted.org/packages/2d/a8/c2c8f29bd18f7ef52de32a6deb9e3ee87ba18b7b2122636aa9f4438cf627/pydantic_core-2.33.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7edbc454a29fc6aeae1e1eecba4f07b63b8d76e76a748532233c4c167b4cb9ea", size = 2041791 }, + { url = "https://files.pythonhosted.org/packages/08/ad/328081b1c82543ae49d0650048305058583c51f1a9a56a0d6e87bb3a2443/pydantic_core-2.33.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ad05b683963f69a1d5d2c2bdab1274a31221ca737dbbceaa32bcb67359453cdd", size = 1873579 }, + { url = "https://files.pythonhosted.org/packages/6e/8a/bc65dbf7e501e88367cdab06a2c1340457c785f0c72288cae737fd80c0fa/pydantic_core-2.33.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df6a94bf9452c6da9b5d76ed229a5683d0306ccb91cca8e1eea883189780d568", size = 1904189 }, + { url = "https://files.pythonhosted.org/packages/9a/db/30ca6aefda211fb01ef185ca73cb7a0c6e7fe952c524025c8782b5acd771/pydantic_core-2.33.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7965c13b3967909a09ecc91f21d09cfc4576bf78140b988904e94f130f188396", size = 2084446 }, + { url = "https://files.pythonhosted.org/packages/f2/89/a12b55286e30c9f476eab7c53c9249ec76faf70430596496ab0309f28629/pydantic_core-2.33.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3f1fdb790440a34f6ecf7679e1863b825cb5ffde858a9197f851168ed08371e5", size = 2118215 }, + { url = "https://files.pythonhosted.org/packages/8e/55/12721c4a8d7951584ad3d9848b44442559cf1876e0bb424148d1060636b3/pydantic_core-2.33.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:5277aec8d879f8d05168fdd17ae811dd313b8ff894aeeaf7cd34ad28b4d77e33", size = 2079963 }, + { url = "https://files.pythonhosted.org/packages/bd/0c/3391bd5d6ff62ea998db94732528d9bc32c560b0ed861c39119759461946/pydantic_core-2.33.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8ab581d3530611897d863d1a649fb0644b860286b4718db919bfd51ece41f10b", size = 2249388 }, + { url = "https://files.pythonhosted.org/packages/d3/5f/3e4feb042998d7886a9b523b372d83955cbc192a07013dcd24276db078ee/pydantic_core-2.33.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0483847fa9ad5e3412265c1bd72aad35235512d9ce9d27d81a56d935ef489672", size = 2255226 }, + { url = "https://files.pythonhosted.org/packages/25/f2/1647933efaaad61846109a27619f3704929e758a09e6431b8f932a053d40/pydantic_core-2.33.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:de9e06abe3cc5ec6a2d5f75bc99b0bdca4f5c719a5b34026f8c57efbdecd2ee3", size = 2081073 }, +] + +[[package]] +name = "pydantic-extra-types" +version = "2.10.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/fa/6b268a47839f8af46ffeb5bb6aee7bded44fbad54e6bf826c11f17aef91a/pydantic_extra_types-2.10.3.tar.gz", hash = "sha256:dcc0a7b90ac9ef1b58876c9b8fdede17fbdde15420de9d571a9fccde2ae175bb", size = 95128 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0a/f6f8e5f79d188e2f3fa9ecfccfa72538b685985dd5c7c2886c67af70e685/pydantic_extra_types-2.10.3-py3-none-any.whl", hash = "sha256:e8b372752b49019cd8249cc192c62a820d8019f5382a8789d0f887338a59c0f3", size = 37175 }, ] [[package]] @@ -650,27 +663,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.11.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/90/61/fb87430f040e4e577e784e325351186976516faef17d6fcd921fe28edfd7/ruff-0.11.2.tar.gz", hash = "sha256:ec47591497d5a1050175bdf4e1a4e6272cddff7da88a2ad595e1e326041d8d94", size = 3857511 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/99/102578506f0f5fa29fd7e0df0a273864f79af044757aef73d1cae0afe6ad/ruff-0.11.2-py3-none-linux_armv6l.whl", hash = "sha256:c69e20ea49e973f3afec2c06376eb56045709f0212615c1adb0eda35e8a4e477", size = 10113146 }, - { url = "https://files.pythonhosted.org/packages/74/ad/5cd4ba58ab602a579997a8494b96f10f316e874d7c435bcc1a92e6da1b12/ruff-0.11.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2c5424cc1c4eb1d8ecabe6d4f1b70470b4f24a0c0171356290b1953ad8f0e272", size = 10867092 }, - { url = "https://files.pythonhosted.org/packages/fc/3e/d3f13619e1d152c7b600a38c1a035e833e794c6625c9a6cea6f63dbf3af4/ruff-0.11.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ecf20854cc73f42171eedb66f006a43d0a21bfb98a2523a809931cda569552d9", size = 10224082 }, - { url = "https://files.pythonhosted.org/packages/90/06/f77b3d790d24a93f38e3806216f263974909888fd1e826717c3ec956bbcd/ruff-0.11.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c543bf65d5d27240321604cee0633a70c6c25c9a2f2492efa9f6d4b8e4199bb", size = 10394818 }, - { url = "https://files.pythonhosted.org/packages/99/7f/78aa431d3ddebfc2418cd95b786642557ba8b3cb578c075239da9ce97ff9/ruff-0.11.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20967168cc21195db5830b9224be0e964cc9c8ecf3b5a9e3ce19876e8d3a96e3", size = 9952251 }, - { url = "https://files.pythonhosted.org/packages/30/3e/f11186d1ddfaca438c3bbff73c6a2fdb5b60e6450cc466129c694b0ab7a2/ruff-0.11.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:955a9ce63483999d9f0b8f0b4a3ad669e53484232853054cc8b9d51ab4c5de74", size = 11563566 }, - { url = "https://files.pythonhosted.org/packages/22/6c/6ca91befbc0a6539ee133d9a9ce60b1a354db12c3c5d11cfdbf77140f851/ruff-0.11.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:86b3a27c38b8fce73bcd262b0de32e9a6801b76d52cdb3ae4c914515f0cef608", size = 12208721 }, - { url = "https://files.pythonhosted.org/packages/19/b0/24516a3b850d55b17c03fc399b681c6a549d06ce665915721dc5d6458a5c/ruff-0.11.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3b66a03b248c9fcd9d64d445bafdf1589326bee6fc5c8e92d7562e58883e30f", size = 11662274 }, - { url = "https://files.pythonhosted.org/packages/d7/65/76be06d28ecb7c6070280cef2bcb20c98fbf99ff60b1c57d2fb9b8771348/ruff-0.11.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0397c2672db015be5aa3d4dac54c69aa012429097ff219392c018e21f5085147", size = 13792284 }, - { url = "https://files.pythonhosted.org/packages/ce/d2/4ceed7147e05852876f3b5f3fdc23f878ce2b7e0b90dd6e698bda3d20787/ruff-0.11.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:869bcf3f9abf6457fbe39b5a37333aa4eecc52a3b99c98827ccc371a8e5b6f1b", size = 11327861 }, - { url = "https://files.pythonhosted.org/packages/c4/78/4935ecba13706fd60ebe0e3dc50371f2bdc3d9bc80e68adc32ff93914534/ruff-0.11.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2a2b50ca35457ba785cd8c93ebbe529467594087b527a08d487cf0ee7b3087e9", size = 10276560 }, - { url = "https://files.pythonhosted.org/packages/81/7f/1b2435c3f5245d410bb5dc80f13ec796454c21fbda12b77d7588d5cf4e29/ruff-0.11.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7c69c74bf53ddcfbc22e6eb2f31211df7f65054bfc1f72288fc71e5f82db3eab", size = 9945091 }, - { url = "https://files.pythonhosted.org/packages/39/c4/692284c07e6bf2b31d82bb8c32f8840f9d0627d92983edaac991a2b66c0a/ruff-0.11.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6e8fb75e14560f7cf53b15bbc55baf5ecbe373dd5f3aab96ff7aa7777edd7630", size = 10977133 }, - { url = "https://files.pythonhosted.org/packages/94/cf/8ab81cb7dd7a3b0a3960c2769825038f3adcd75faf46dd6376086df8b128/ruff-0.11.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:842a472d7b4d6f5924e9297aa38149e5dcb1e628773b70e6387ae2c97a63c58f", size = 11378514 }, - { url = "https://files.pythonhosted.org/packages/d9/3a/a647fa4f316482dacf2fd68e8a386327a33d6eabd8eb2f9a0c3d291ec549/ruff-0.11.2-py3-none-win32.whl", hash = "sha256:aca01ccd0eb5eb7156b324cfaa088586f06a86d9e5314b0eb330cb48415097cc", size = 10319835 }, - { url = "https://files.pythonhosted.org/packages/86/54/3c12d3af58012a5e2cd7ebdbe9983f4834af3f8cbea0e8a8c74fa1e23b2b/ruff-0.11.2-py3-none-win_amd64.whl", hash = "sha256:3170150172a8f994136c0c66f494edf199a0bbea7a409f649e4bc8f4d7084080", size = 11373713 }, - { url = "https://files.pythonhosted.org/packages/d6/d4/dd813703af8a1e2ac33bf3feb27e8a5ad514c9f219df80c64d69807e7f71/ruff-0.11.2-py3-none-win_arm64.whl", hash = "sha256:52933095158ff328f4c77af3d74f0379e34fd52f175144cefc1b192e7ccd32b4", size = 10441990 }, +version = "0.11.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/5b/3ae20f89777115944e89c2d8c2e795dcc5b9e04052f76d5347e35e0da66e/ruff-0.11.4.tar.gz", hash = "sha256:f45bd2fb1a56a5a85fae3b95add03fb185a0b30cf47f5edc92aa0355ca1d7407", size = 3933063 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/db/baee59ac88f57527fcbaad3a7b309994e42329c6bc4d4d2b681a3d7b5426/ruff-0.11.4-py3-none-linux_armv6l.whl", hash = "sha256:d9f4a761ecbde448a2d3e12fb398647c7f0bf526dbc354a643ec505965824ed2", size = 10106493 }, + { url = "https://files.pythonhosted.org/packages/c1/d6/9a0962cbb347f4ff98b33d699bf1193ff04ca93bed4b4222fd881b502154/ruff-0.11.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8c1747d903447d45ca3d40c794d1a56458c51e5cc1bc77b7b64bd2cf0b1626cc", size = 10876382 }, + { url = "https://files.pythonhosted.org/packages/3a/8f/62bab0c7d7e1ae3707b69b157701b41c1ccab8f83e8501734d12ea8a839f/ruff-0.11.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:51a6494209cacca79e121e9b244dc30d3414dac8cc5afb93f852173a2ecfc906", size = 10237050 }, + { url = "https://files.pythonhosted.org/packages/09/96/e296965ae9705af19c265d4d441958ed65c0c58fc4ec340c27cc9d2a1f5b/ruff-0.11.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f171605f65f4fc49c87f41b456e882cd0c89e4ac9d58e149a2b07930e1d466f", size = 10424984 }, + { url = "https://files.pythonhosted.org/packages/e5/56/644595eb57d855afed6e54b852e2df8cd5ca94c78043b2f29bdfb29882d5/ruff-0.11.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ebf99ea9af918878e6ce42098981fc8c1db3850fef2f1ada69fb1dcdb0f8e79e", size = 9957438 }, + { url = "https://files.pythonhosted.org/packages/86/83/9d3f3bed0118aef3e871ded9e5687fb8c5776bde233427fd9ce0a45db2d4/ruff-0.11.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edad2eac42279df12e176564a23fc6f4aaeeb09abba840627780b1bb11a9d223", size = 11547282 }, + { url = "https://files.pythonhosted.org/packages/40/e6/0c6e4f5ae72fac5ccb44d72c0111f294a5c2c8cc5024afcb38e6bda5f4b3/ruff-0.11.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f103a848be9ff379fc19b5d656c1f911d0a0b4e3e0424f9532ececf319a4296e", size = 12182020 }, + { url = "https://files.pythonhosted.org/packages/b5/92/4aed0e460aeb1df5ea0c2fbe8d04f9725cccdb25d8da09a0d3f5b8764bf8/ruff-0.11.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:193e6fac6eb60cc97b9f728e953c21cc38a20077ed64f912e9d62b97487f3f2d", size = 11679154 }, + { url = "https://files.pythonhosted.org/packages/1b/d3/7316aa2609f2c592038e2543483eafbc62a0e1a6a6965178e284808c095c/ruff-0.11.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7af4e5f69b7c138be8dcffa5b4a061bf6ba6a3301f632a6bce25d45daff9bc99", size = 13905985 }, + { url = "https://files.pythonhosted.org/packages/63/80/734d3d17546e47ff99871f44ea7540ad2bbd7a480ed197fe8a1c8a261075/ruff-0.11.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:126b1bf13154aa18ae2d6c3c5efe144ec14b97c60844cfa6eb960c2a05188222", size = 11348343 }, + { url = "https://files.pythonhosted.org/packages/04/7b/70fc7f09a0161dce9613a4671d198f609e653d6f4ff9eee14d64c4c240fb/ruff-0.11.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8806daaf9dfa881a0ed603f8a0e364e4f11b6ed461b56cae2b1c0cab0645304", size = 10308487 }, + { url = "https://files.pythonhosted.org/packages/1a/22/1cdd62dabd678d75842bf4944fd889cf794dc9e58c18cc547f9eb28f95ed/ruff-0.11.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5d94bb1cc2fc94a769b0eb975344f1b1f3d294da1da9ddbb5a77665feb3a3019", size = 9929091 }, + { url = "https://files.pythonhosted.org/packages/9f/20/40e0563506332313148e783bbc1e4276d657962cc370657b2fff20e6e058/ruff-0.11.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:995071203d0fe2183fc7a268766fd7603afb9996785f086b0d76edee8755c896", size = 10924659 }, + { url = "https://files.pythonhosted.org/packages/b5/41/eef9b7aac8819d9e942f617f9db296f13d2c4576806d604aba8db5a753f1/ruff-0.11.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7a37ca937e307ea18156e775a6ac6e02f34b99e8c23fe63c1996185a4efe0751", size = 11428160 }, + { url = "https://files.pythonhosted.org/packages/ff/61/c488943414fb2b8754c02f3879de003e26efdd20f38167ded3fb3fc1cda3/ruff-0.11.4-py3-none-win32.whl", hash = "sha256:0e9365a7dff9b93af933dab8aebce53b72d8f815e131796268709890b4a83270", size = 10311496 }, + { url = "https://files.pythonhosted.org/packages/b6/2b/2a1c8deb5f5dfa3871eb7daa41492c4d2b2824a74d2b38e788617612a66d/ruff-0.11.4-py3-none-win_amd64.whl", hash = "sha256:5a9fa1c69c7815e39fcfb3646bbfd7f528fa8e2d4bebdcf4c2bd0fa037a255fb", size = 11399146 }, + { url = "https://files.pythonhosted.org/packages/4f/03/3aec4846226d54a37822e4c7ea39489e4abd6f88388fba74e3d4abe77300/ruff-0.11.4-py3-none-win_arm64.whl", hash = "sha256:d435db6b9b93d02934cf61ef332e66af82da6d8c69aefdea5994c89997c7a0fc", size = 10450306 }, ] [[package]] @@ -746,11 +759,11 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.13.0" +version = "4.13.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0e/3e/b00a62db91a83fff600de219b6ea9908e6918664899a2d85db222f4fbf19/typing_extensions-4.13.0.tar.gz", hash = "sha256:0a4ac55a5820789d87e297727d229866c9650f6521b64206413c4fbada24d95b", size = 106520 } +sdist = { url = "https://files.pythonhosted.org/packages/76/ad/cd3e3465232ec2416ae9b983f27b9e94dc8171d56ac99b345319a9475967/typing_extensions-4.13.1.tar.gz", hash = "sha256:98795af00fb9640edec5b8e31fc647597b4691f099ad75f469a2616be1a76dff", size = 106633 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/86/39b65d676ec5732de17b7e3c476e45bb80ec64eb50737a8dce1a4178aba1/typing_extensions-4.13.0-py3-none-any.whl", hash = "sha256:c8dd92cc0d6425a97c18fbb9d1954e5ff92c1ca881a309c45f06ebc0b79058e5", size = 45683 }, + { url = "https://files.pythonhosted.org/packages/df/c5/e7a0b0f5ed69f94c8ab7379c599e6036886bffcde609969a5325f47f1332/typing_extensions-4.13.1-py3-none-any.whl", hash = "sha256:4b6cf02909eb5495cfbc3f6e8fd49217e6cc7944e145cdda8caa3734777f9e69", size = 45739 }, ] [[package]] @@ -767,11 +780,12 @@ wheels = [ [[package]] name = "vehiclepass" -version = "0.2.0" +version = "0.3.0" source = { editable = "." } dependencies = [ { name = "httpx" }, { name = "pydantic" }, + { name = "pydantic-extra-types" }, { name = "python-dotenv" }, ] @@ -789,6 +803,7 @@ dev = [ requires-dist = [ { name = "httpx", specifier = ">=0.28.1" }, { name = "pydantic", specifier = ">=2.11.1" }, + { name = "pydantic-extra-types", specifier = ">=2.10.3" }, { name = "python-dotenv", specifier = ">=1.1.0" }, ]