diff --git a/eslint.config.js b/eslint.config.js index d65de095..b7a91583 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,9 +1,9 @@ -import globals from "globals"; import pluginJs from "@eslint/js"; -import tseslint from "typescript-eslint"; -import pluginReact from "eslint-plugin-react"; import eslintConfigPrettier from "eslint-config-prettier"; import pluginImport from "eslint-plugin-import"; +import pluginReact from "eslint-plugin-react"; +import globals from "globals"; +import tseslint from "typescript-eslint"; export default [ { files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"] }, diff --git a/lonboard/_map.py b/lonboard/_map.py index 579fa0fc..d80c7364 100644 --- a/lonboard/_map.py +++ b/lonboard/_map.py @@ -20,6 +20,7 @@ VariableLengthTuple, ViewStateTrait, ) +from lonboard.view import BaseView if TYPE_CHECKING: import sys @@ -151,6 +152,8 @@ def on_click(self, callback: Callable, *, remove: bool = False) -> None: _esm = bundler_output_dir / "index.js" _css = bundler_output_dir / "index.css" + # TODO: change this view state to allow non-map view states if we have non-map views + # Also allow a list/tuple of view states for multiple views view_state = ViewStateTrait() """ The view state of the map. @@ -174,6 +177,7 @@ def on_click(self, callback: Callable, *, remove: bool = False) -> None: once it's been initially rendered. """ + _has_click_handlers = t.Bool(default_value=False, allow_none=False).tag(sync=True) """ Indicates if a click handler has been registered. @@ -192,6 +196,15 @@ def on_click(self, callback: Callable, *, remove: bool = False) -> None: """One or more `Layer` objects to display on this map. """ + views: t.Instance[BaseView | None] = t.Instance(BaseView, allow_none=True).tag( + sync=True, + **ipywidgets.widget_serialization, + ) + """A View instance. + + Views represent the "camera(s)" (essentially viewport dimensions and projection matrices) that you look at your data with. deck.gl offers multiple view types for both geospatial and non-geospatial use cases. Read the [Views and Projections](https://deck.gl/docs/developer-guide/views) guide for the concept and examples. + """ + show_tooltip = t.Bool(default_value=False).tag(sync=True) """ Whether to render a tooltip on hover on the map. @@ -221,6 +234,9 @@ def on_click(self, callback: Callable, *, remove: bool = False) -> None: basemap: t.Instance[MaplibreBasemap | None] = t.Instance( MaplibreBasemap, + # If both `args` and `kw` are None, then the default value is None. + # Set empty kw so that the default is MaplibreBasemap() with default params + kw={}, allow_none=True, ).tag( sync=True, diff --git a/lonboard/_serialization.py b/lonboard/_serialization.py index 04a3fd10..478f30c5 100644 --- a/lonboard/_serialization.py +++ b/lonboard/_serialization.py @@ -22,6 +22,8 @@ from lonboard._utils import timestamp_start_offset if TYPE_CHECKING: + from pydantic import BaseModel + from lonboard._layer import BaseArrowLayer from lonboard.experimental._layer import TripsLayer from lonboard.models import ViewState @@ -159,13 +161,17 @@ def validate_accessor_length_matches_table( raise TraitError("accessor must have same length as table") -def serialize_view_state(data: ViewState | None, obj: Any) -> None | dict[str, Any]: # noqa: ARG001 +def serialize_view_state(data: ViewState | None, _obj: Any) -> None | dict[str, Any]: if data is None: return None return data._asdict() +def serialize_pydantic_model(data: BaseModel, _obj: Any) -> None | dict[str, Any]: + return data.model_dump(exclude_unset=True, exclude_none=True) + + def serialize_timestamp_accessor( timestamps: ChunkedArray, obj: TripsLayer, diff --git a/lonboard/models.py b/lonboard/models.py index a9204078..a7e1dcd7 100644 --- a/lonboard/models.py +++ b/lonboard/models.py @@ -1,5 +1,8 @@ from typing import NamedTuple +from anywidget.experimental import MimeBundleDescriptor +from pydantic import BaseModel, ConfigDict + class ViewState(NamedTuple): """State of a view position of a map.""" @@ -18,3 +21,55 @@ class ViewState(NamedTuple): bearing: float """Bearing angle in degrees. `0` is north.""" + + +def _to_camel(string: str) -> str: + parts = string.split("_") + return parts[0] + "".join(word.capitalize() for word in parts[1:]) + + +class MapViewState(BaseModel, frozen=True): + """State of a map view.""" + + _repr_mimebundle_ = MimeBundleDescriptor(no_view=True) + + longitude: float + """Longitude of the map center""" + + latitude: float + """Latitude of the map center.""" + + zoom: float + """Zoom level.""" + + pitch: float | None = None + """Pitch (tilt) of the map, in degrees. `0` looks top down""" + + bearing: float | None = None + """Bearing (rotation) of the map, in degrees. `0` is north up""" + + min_zoom: float | None = None + """Min zoom, default `0`""" + + max_zoom: float | None = None + """Max zoom, default `20`""" + + min_pitch: float | None = None + """Min pitch, default `0`""" + + max_pitch: float | None = None + """Max pitch, default `60`""" + + position: list[float] | None = None + """Viewport center offsets from lng, lat in meters""" + + near_z: float | None = None + """The near plane position""" + + far_z: float | None = None + """The far plane position""" + + model_config = ConfigDict( + alias_generator=_to_camel, + populate_by_name=True, + ) diff --git a/lonboard/traits.py b/lonboard/traits.py index fba25a4d..c99f454e 100644 --- a/lonboard/traits.py +++ b/lonboard/traits.py @@ -10,7 +10,7 @@ import sys import warnings -from typing import TYPE_CHECKING, Any, NoReturn, TypeVar +from typing import TYPE_CHECKING, Any, Generic, NoReturn, TypeVar from typing import cast as type_cast from urllib.parse import urlparse @@ -24,6 +24,7 @@ Table, fixed_size_list_array, ) +from pydantic import BaseModel, ValidationError from traitlets import TraitError, Undefined from traitlets.utils.descriptions import class_of, describe @@ -34,6 +35,7 @@ from lonboard._serialization import ( ACCESSOR_SERIALIZATION, TABLE_SERIALIZATION, + serialize_pydantic_model, serialize_view_state, ) from lonboard._utils import get_geometry_column_index @@ -47,6 +49,7 @@ from traitlets.utils.sentinel import Sentinel from lonboard._layer import BaseArrowLayer + from lonboard._map import Map DEFAULT_INITIAL_VIEW_STATE = { "latitude": 10, @@ -444,6 +447,34 @@ def validate(self, obj: BaseArrowLayer, value: Any) -> float | ChunkedArray: return value.rechunk(max_chunksize=obj._rows_per_chunk) +PydanticModelT = TypeVar("PydanticModelT", bound=BaseModel) + + +class PydanticModelTrait(FixedErrorTraitType, Generic[PydanticModelT]): + """A trait to validate input for a pydantic model. + + The pydantic model must be a subclass of `pydantic.BaseModel`. + """ + + klass: type[PydanticModelT] + + def __init__( + self: TraitType, + klass: type[PydanticModelT], + *args: Any, + **kwargs: Any, + ) -> None: + self.klass = klass # type: ignore[assignment] + super().__init__(*args, **kwargs) + self.tag(sync=True, to_json=serialize_pydantic_model) + + def validate(self, obj: HasTraits, value: dict[str, Any]) -> BaseModel: + try: + return self.klass(**value) + except ValidationError as e: + self.error(obj, value, error=e) + + class TextAccessor(FixedErrorTraitType): """A trait to validate input for a deck.gl text accessor. @@ -943,7 +974,7 @@ def __init__( self.tag(sync=True, to_json=serialize_view_state) - def validate(self, obj: Any, value: Any) -> None | ViewState: + def validate(self, obj: Map, value: Any) -> None | ViewState: if value is None: return None diff --git a/lonboard/types/map.py b/lonboard/types/map.py index 57f7f89f..37b0d71e 100644 --- a/lonboard/types/map.py +++ b/lonboard/types/map.py @@ -10,6 +10,7 @@ if TYPE_CHECKING: from lonboard.basemap import MaplibreBasemap + from lonboard.view import BaseView class MapKwargs(TypedDict, total=False): @@ -22,4 +23,5 @@ class MapKwargs(TypedDict, total=False): show_tooltip: bool show_side_panel: bool use_device_pixels: int | float | bool + views: BaseView | list[BaseView] | tuple[BaseView, ...] view_state: dict[str, Any] diff --git a/lonboard/view.py b/lonboard/view.py new file mode 100644 index 00000000..0a2155ff --- /dev/null +++ b/lonboard/view.py @@ -0,0 +1,253 @@ +import traitlets as t + +from lonboard._base import BaseWidget + + +class BaseView(BaseWidget): + """A deck.gl View. + + The `View` class and its subclasses are used to specify where and how your deck.gl layers should be rendered. Applications typically instantiate at least one `View` subclass. + + """ + + x = t.Union([t.Int(), t.Unicode()], allow_none=True, default_value=None).tag( + sync=True, + ) + """The x position of the view. + + A relative (e.g. `'50%'`) or absolute position. Default `0`. + """ + + y = t.Union([t.Int(), t.Unicode()], allow_none=True, default_value=None).tag( + sync=True, + ) + """The y position of the view. + + A relative (e.g. `'50%'`) or absolute position. Default `0`. + """ + + width = t.Union([t.Int(), t.Unicode()], allow_none=True, default_value=None).tag( + sync=True, + ) + """The width of the view. + + A relative (e.g. `'50%'`) or absolute extent. Default `'100%'`. + """ + + height = t.Union([t.Int(), t.Unicode()], allow_none=True, default_value=None).tag( + sync=True, + ) + """The height of the view. + + A relative (e.g. `'50%'`) or absolute extent. Default `'100%'`. + """ + + +class FirstPersonView(BaseView): + """A deck.gl FirstPersonView. + + The `FirstPersonView` class is a subclass of `View` that describes a camera placed at a provided location, looking towards the direction and orientation specified by viewState. The behavior is similar to that of a first-person game. + """ + + _view_type = t.Unicode("first-person-view").tag(sync=True) + + projection_matrix = t.List( + t.Float(), + allow_none=True, + default_value=None, + minlen=16, + maxlen=16, + ).tag( + sync=True, + ) + """Projection matrix. + + If `projectionMatrix` is not supplied, the `View` class will build a projection matrix from the following parameters: + """ + + fovy = t.Float(allow_none=True, default_value=None).tag(sync=True) + """Field of view covered by camera, in the perspective case. In degrees. + + Default `50`. + """ + + near = t.Float(allow_none=True, default_value=None).tag(sync=True) + """Distance of near clipping plane. + + Default `0.1`. + """ + + far = t.Float(allow_none=True, default_value=None).tag(sync=True) + """Distance of far clipping plane. + + Default `1000`. + """ + + focal_distance = t.Float(allow_none=True, default_value=None).tag(sync=True) + """Modifier of viewport scale. + + Corresponds to the number of pixels per meter. Default `1`. + """ + + +class GlobeView(BaseView): + """A deck.gl GlobeView. + + The `GlobeView` class is a subclass of `View`. This view projects the earth into a 3D globe. + """ + + _view_type = t.Unicode("globe-view").tag(sync=True) + + resolution = t.Float(allow_none=True, default_value=None).tag(sync=True) + """The resolution at which to turn flat features into 3D meshes, in degrees. + + Smaller numbers will generate more detailed mesh. Default `10`. + """ + + near_z_multiplier = t.Float(allow_none=True, default_value=None).tag(sync=True) + """Scaler for the near plane, 1 unit equals to the height of the viewport. + + Default to `0.1`. Overwrites the `near` parameter. + """ + + far_z_multiplier = t.Float(allow_none=True, default_value=None).tag(sync=True) + """Scaler for the far plane, 1 unit equals to the distance from the camera to the top edge of the screen. + + Default to `2`. Overwrites the `far` parameter. + """ + + +class MapView(BaseView): + """A deck.gl MapView. + + The `MapView` class is a subclass of `View`. This viewport creates a camera that looks at a geospatial location on a map from a certain direction. The behavior of `MapView` is generally modeled after that of Mapbox GL JS. + """ + + _view_type = t.Unicode("map-view").tag(sync=True) + + repeat = t.Bool(allow_none=True, default_value=None).tag(sync=True) + """ + Whether to render multiple copies of the map at low zoom levels. Default `false`. + """ + + near_z_multiplier = t.Float(allow_none=True, default_value=None).tag(sync=True) + """Scaler for the near plane, 1 unit equals to the height of the viewport. + + Default to `0.1`. Overwrites the `near` parameter. + """ + + far_z_multiplier = t.Float(allow_none=True, default_value=None).tag(sync=True) + """Scaler for the far plane, 1 unit equals to the distance from the camera to the top edge of the screen. + + Default to `1.01`. Overwrites the `far` parameter. + """ + + projection_matrix = t.List( + t.Float(), + allow_none=True, + default_value=None, + minlen=16, + maxlen=16, + ).tag( + sync=True, + ) + """Projection matrix. + + If `projectionMatrix` is not supplied, the `View` class will build a projection matrix from the following parameters: + """ + + fovy = t.Float(allow_none=True, default_value=None).tag(sync=True) + """Field of view covered by camera, in the perspective case. In degrees. + + If not supplied, will be calculated from `altitude`. + """ + + altitude = t.Float(allow_none=True, default_value=None).tag(sync=True) + """Distance of the camera relative to viewport height. + + Default `1.5`. + """ + + orthographic = t.Bool(allow_none=True, default_value=None).tag(sync=True) + """Whether to create an orthographic or perspective projection matrix. + + Default is `false` (perspective projection). + """ + + +class OrbitView(BaseView): + """A deck.gl OrbitView. + + The `OrbitView` class is a subclass of `View` that creates a 3D camera that rotates around a target position. It is usually used for the examination of a 3D scene in non-geospatial use cases. + """ + + _view_type = t.Unicode("orbit-view").tag(sync=True) + + orbit_axis = t.Unicode(allow_none=True, default_value=None).tag(sync=True) + """Axis with 360 degrees rotating freedom, either `'Y'` or `'Z'`, default to `'Z'`.""" + + projection_matrix = t.List( + t.Float(), + allow_none=True, + default_value=None, + minlen=16, + maxlen=16, + ).tag( + sync=True, + ) + """Projection matrix. + + If `projectionMatrix` is not supplied, the `View` class will build a projection matrix from the following parameters: + """ + + fovy = t.Float(allow_none=True, default_value=None).tag(sync=True) + """Field of view covered by camera, in the perspective case. In degrees. + + Default `50`. + """ + + near = t.Float(allow_none=True, default_value=None).tag(sync=True) + """Distance of near clipping plane. + + Default `0.1`. + """ + + far = t.Float(allow_none=True, default_value=None).tag(sync=True) + """Distance of far clipping plane. + + Default `1000`. + """ + + orthographic = t.Bool(allow_none=True, default_value=None).tag(sync=True) + """Whether to create an orthographic or perspective projection matrix. + + Default is `false` (perspective projection). + """ + + +class OrthographicView(BaseView): + """A deck.gl OrthographicView. + + The `OrthographicView` class is a subclass of `View` that creates a top-down view of the XY plane. It is usually used for rendering 2D charts in non-geospatial use cases. + """ + + _view_type = t.Unicode("orthographic-view").tag(sync=True) + + flip_y = t.Bool(allow_none=True, default_value=None).tag(sync=True) + """ + Whether to use top-left coordinates (`true`) or bottom-left coordinates (`false`). + + Default `true`. + """ + + near = t.Float(allow_none=True, default_value=None).tag(sync=True) + """Distance of near clipping plane. + + Default `0.1`. + """ + + far = t.Float(allow_none=True, default_value=None).tag(sync=True) + """Distance of far clipping plane. + + Default `1000`. + """ diff --git a/package-lock.json b/package-lock.json index 3d16a3ee..f8b6c0a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16494,6 +16494,7 @@ "integrity": "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", diff --git a/pyproject.toml b/pyproject.toml index 6fe8e14d..59343927 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ dependencies = [ "pyproj>=3.3", "typing-extensions>=4.6.0; python_version < '3.12'", "geoarrow-rust-core>=0.5.2", + "pydantic>=2.12.2", ] keywords = [ "GIS", diff --git a/src/index.tsx b/src/index.tsx index 5d8f20e3..704539c1 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -17,6 +17,7 @@ import { initializeChildModels, } from "./model/index.js"; import { loadModel } from "./model/initialize.js"; +import { BaseViewModel, initializeView } from "./model/view.js"; import { initParquetWasm } from "./parquet.js"; import DeckFirstRenderer from "./renderers/deck-first.js"; import OverlayRenderer from "./renderers/overlay.js"; @@ -30,7 +31,7 @@ import { useViewStateDebounced } from "./state"; import Toolbar from "./toolbar.js"; import { getTooltip } from "./tooltip/index.js"; import { Message } from "./types.js"; -import { isDefined } from "./util.js"; +import { isDefined, isGlobeView } from "./util.js"; import { MachineContext, MachineProvider } from "./xstate"; import * as selectors from "./xstate/selectors"; @@ -90,6 +91,9 @@ function App() { ); const [parameters] = useModelState("parameters"); const [customAttribution] = useModelState("custom_attribution"); + const [mapId] = useState(uuidv4()); + const [childLayerIds] = useModelState("layers"); + const [viewIds] = useModelState("views"); // initialViewState is the value of view_state on the Python side. This is // called `initial` here because it gets passed in to deck's @@ -115,22 +119,19 @@ function App() { } }); - const [mapId] = useState(uuidv4()); - const [layersState, setLayersState] = useState< - Record - >({}); + // Fake state just to get react to re-render when a model callback is called + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [stateCounter, setStateCounter] = useState(new Date()); + const updateStateCallback = () => setStateCounter(new Date()); - const [childLayerIds] = useModelState("layers"); + ////////////////////// + // Basemap state + ////////////////////// const [basemapState, setBasemapState] = useState( null, ); - // Fake state just to get react to re-render when a model callback is called - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [stateCounter, setStateCounter] = useState(new Date()); - const updateStateCallback = () => setStateCounter(new Date()); - useEffect(() => { const loadBasemap = async () => { try { @@ -156,6 +157,14 @@ function App() { loadBasemap(); }, [basemapModelId]); + ////////////////////// + // Layers state + ////////////////////// + + const [layersState, setLayersState] = useState< + Record + >({}); + useEffect(() => { const loadAndUpdateLayers = async () => { try { @@ -192,6 +201,48 @@ function App() { layerModel.render(), ); + ////////////////////// + // Views state + ////////////////////// + + const [viewsState, setViewsState] = useState< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + Record> + >({}); + + useEffect(() => { + const loadAndUpdateViews = async () => { + try { + if (!viewIds) { + setViewsState({}); + return; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const viewsModels = await initializeChildModels>( + model.widget_manager as IWidgetManager, + typeof viewIds === "string" ? [viewIds] : viewIds, + viewsState, + async (model: WidgetModel) => + initializeView(model, updateStateCallback), + ); + + setViewsState(viewsModels); + } catch (error) { + console.error("Error loading child views:", error); + } + }; + + loadAndUpdateViews(); + }, [viewIds]); + + const _deckViews = Object.values(viewsState).map((viewModel) => + viewModel.build(), + ); + // When the user hasn't specified any views, we let deck.gl create + // a default view, and so set undefined here. + const views = _deckViews.length > 0 ? _deckViews : undefined; + const onMapClickHandler = useCallback((info: PickingInfo) => { // We added this flag to prevent the hover event from firing after a // click event. @@ -261,6 +312,7 @@ function App() { } }, parameters: parameters || {}, + views, }; const overlayRenderProps: OverlayRendererProps = { @@ -283,7 +335,16 @@ function App() {
diff --git a/src/model/base.ts b/src/model/base.ts index 0d9faa08..39710dee 100644 --- a/src/model/base.ts +++ b/src/model/base.ts @@ -17,10 +17,6 @@ export abstract class BaseModel { this.callbacks.set("change", updateStateCallback); } - async loadSubModels() { - return; - } - /** * Initialize an attribute that does not need any transformation from its * serialized representation to its deck.gl representation. diff --git a/src/model/extension.ts b/src/model/extension.ts index 7ae75f7b..ede75be1 100644 --- a/src/model/extension.ts +++ b/src/model/extension.ts @@ -258,6 +258,5 @@ export async function initializeExtension( throw new Error(`no known model for extension type ${extensionType}`); } - await extensionModel.loadSubModels(); return extensionModel; } diff --git a/src/model/view.ts b/src/model/view.ts new file mode 100644 index 00000000..219e5d25 --- /dev/null +++ b/src/model/view.ts @@ -0,0 +1,283 @@ +import { + FirstPersonView, + FirstPersonViewProps, + FirstPersonViewState, + _GlobeView as GlobeView, + GlobeViewProps, + GlobeViewState, + MapView, + MapViewProps, + MapViewState, + OrbitView, + OrbitViewProps, + OrbitViewState, + OrthographicView, + OrthographicViewProps, + OrthographicViewState, +} from "@deck.gl/core"; +import type { View } from "@deck.gl/core"; +import { CommonViewProps } from "@deck.gl/core/dist/views/view"; +import { WidgetModel } from "@jupyter-widgets/base"; + +import { isDefined } from "../util"; +import { BaseModel } from "./base"; + +export abstract class BaseViewModel extends BaseModel { + protected x: CommonViewProps["x"] | null; + protected y: CommonViewProps["y"] | null; + protected width: CommonViewProps["width"] | null; + protected height: CommonViewProps["height"] | null; + protected padding: CommonViewProps["padding"] | null; + protected controller: CommonViewProps["controller"] | null; + + constructor(model: WidgetModel, updateStateCallback: () => void) { + super(model, updateStateCallback); + + this.initRegularAttribute("x", "x"); + this.initRegularAttribute("y", "y"); + this.initRegularAttribute("width", "width"); + this.initRegularAttribute("height", "height"); + this.initRegularAttribute("padding", "padding"); + } + + baseViewProps(): CommonViewProps { + return { + id: this.model.model_id, + ...(isDefined(this.x) && { x: this.x }), + ...(isDefined(this.y) && { y: this.y }), + ...(isDefined(this.width) && { width: this.width }), + ...(isDefined(this.height) && { height: this.height }), + ...(isDefined(this.padding) && { padding: this.padding }), + ...(isDefined(this.controller) && { controller: this.controller }), + }; + } + + abstract viewProps(): Omit, "id">; + + abstract build(): View; +} + +export class FirstPersonViewModel extends BaseViewModel { + static viewType = "first-person-view"; + + protected projectionMatrix: FirstPersonViewProps["projectionMatrix"] | null; + protected fovy: FirstPersonViewProps["fovy"] | null; + protected near: FirstPersonViewProps["near"] | null; + protected far: FirstPersonViewProps["far"] | null; + protected focalDistance: FirstPersonViewProps["focalDistance"] | null; + + constructor(model: WidgetModel, updateStateCallback: () => void) { + super(model, updateStateCallback); + + this.initRegularAttribute("projection_matrix", "projectionMatrix"); + this.initRegularAttribute("fovy", "fovy"); + this.initRegularAttribute("near", "near"); + this.initRegularAttribute("far", "far"); + this.initRegularAttribute("focal_distance", "focalDistance"); + } + + viewProps(): Omit { + return { + ...(isDefined(this.projectionMatrix) && { + projectionMatrix: this.projectionMatrix, + }), + ...(isDefined(this.fovy) && { fovy: this.fovy }), + ...(isDefined(this.near) && { near: this.near }), + ...(isDefined(this.far) && { far: this.far }), + ...(isDefined(this.focalDistance) && { + focalDistance: this.focalDistance, + }), + }; + } + + build(): FirstPersonView { + return new FirstPersonView({ + ...this.baseViewProps(), + ...this.viewProps(), + }); + } +} + +export class GlobeViewModel extends BaseViewModel { + static viewType = "globe-view"; + + protected resolution: GlobeViewProps["resolution"] | null; + protected nearZMultiplier: GlobeViewProps["nearZMultiplier"] | null; + protected farZMultiplier: GlobeViewProps["farZMultiplier"] | null; + + constructor(model: WidgetModel, updateStateCallback: () => void) { + super(model, updateStateCallback); + + this.initRegularAttribute("resolution", "resolution"); + this.initRegularAttribute("near_z_multiplier", "nearZMultiplier"); + this.initRegularAttribute("far_z_multiplier", "farZMultiplier"); + } + + viewProps(): Omit { + return { + ...(isDefined(this.resolution) && { resolution: this.resolution }), + ...(isDefined(this.nearZMultiplier) && { + nearZMultiplier: this.nearZMultiplier, + }), + ...(isDefined(this.farZMultiplier) && { + farZMultiplier: this.farZMultiplier, + }), + }; + } + + build(): GlobeView { + return new GlobeView({ + ...this.baseViewProps(), + ...this.viewProps(), + }); + } +} + +export class MapViewModel extends BaseViewModel { + static viewType = "map-view"; + + protected repeat: MapViewProps["repeat"] | null; + protected nearZMultiplier: MapViewProps["nearZMultiplier"] | null; + protected farZMultiplier: MapViewProps["farZMultiplier"] | null; + protected projectionMatrix: MapViewProps["projectionMatrix"] | null; + protected fovy: MapViewProps["fovy"] | null; + protected altitude: MapViewProps["altitude"] | null; + protected orthographic: MapViewProps["orthographic"] | null; + + constructor(model: WidgetModel, updateStateCallback: () => void) { + super(model, updateStateCallback); + + this.initRegularAttribute("repeat", "repeat"); + this.initRegularAttribute("near_z_multiplier", "nearZMultiplier"); + this.initRegularAttribute("far_z_multiplier", "farZMultiplier"); + this.initRegularAttribute("projection_matrix", "projectionMatrix"); + this.initRegularAttribute("fovy", "fovy"); + this.initRegularAttribute("altitude", "altitude"); + this.initRegularAttribute("orthographic", "orthographic"); + } + + viewProps(): Omit { + return { + ...(isDefined(this.repeat) && { repeat: this.repeat }), + ...(isDefined(this.nearZMultiplier) && { + nearZMultiplier: this.nearZMultiplier, + }), + ...(isDefined(this.farZMultiplier) && { + farZMultiplier: this.farZMultiplier, + }), + ...(isDefined(this.projectionMatrix) && { + projectionMatrix: this.projectionMatrix, + }), + ...(isDefined(this.fovy) && { fovy: this.fovy }), + ...(isDefined(this.altitude) && { altitude: this.altitude }), + ...(isDefined(this.orthographic) && { orthographic: this.orthographic }), + }; + } + + build(): MapView { + return new MapView({ + ...this.baseViewProps(), + ...this.viewProps(), + }); + } +} + +export class OrbitViewModel extends BaseViewModel { + static viewType = "orbit-view"; + + protected orbitAxis: OrbitViewProps["orbitAxis"] | null; + protected projectionMatrix: OrbitViewProps["projectionMatrix"] | null; + protected fovy: OrbitViewProps["fovy"] | null; + protected near: OrbitViewProps["near"] | null; + protected far: OrbitViewProps["far"] | null; + protected orthographic: OrbitViewProps["orthographic"] | null; + + constructor(model: WidgetModel, updateStateCallback: () => void) { + super(model, updateStateCallback); + + this.initRegularAttribute("orbit_axis", "orbitAxis"); + this.initRegularAttribute("projection_matrix", "projectionMatrix"); + this.initRegularAttribute("fovy", "fovy"); + this.initRegularAttribute("near", "near"); + this.initRegularAttribute("far", "far"); + this.initRegularAttribute("orthographic", "orthographic"); + } + + viewProps(): Omit { + return { + ...(isDefined(this.orbitAxis) && { orbitAxis: this.orbitAxis }), + ...(isDefined(this.projectionMatrix) && { + projectionMatrix: this.projectionMatrix, + }), + ...(isDefined(this.fovy) && { fovy: this.fovy }), + ...(isDefined(this.near) && { near: this.near }), + ...(isDefined(this.far) && { far: this.far }), + ...(isDefined(this.orthographic) && { orthographic: this.orthographic }), + }; + } + + build(): OrbitView { + return new OrbitView({ + ...this.baseViewProps(), + ...this.viewProps(), + }); + } +} + +export class OrthographicViewModel extends BaseViewModel { + static viewType = "orthographic-view"; + + protected flipY: OrthographicViewProps["flipY"] | null; + protected near: OrthographicViewProps["near"] | null; + protected far: OrthographicViewProps["far"] | null; + + constructor(model: WidgetModel, updateStateCallback: () => void) { + super(model, updateStateCallback); + + this.initRegularAttribute("flip_y", "flipY"); + this.initRegularAttribute("near", "near"); + this.initRegularAttribute("far", "far"); + } + + viewProps(): Omit { + return { + ...(isDefined(this.flipY) && { flipY: this.flipY }), + ...(isDefined(this.near) && { near: this.near }), + ...(isDefined(this.far) && { far: this.far }), + }; + } + + build(): OrthographicView { + return new OrthographicView({ + ...this.baseViewProps(), + ...this.viewProps(), + }); + } +} + +export async function initializeView( + model: WidgetModel, + updateStateCallback: () => void, + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): Promise> { + const viewType = model.get("_view_type"); + switch (viewType) { + case FirstPersonViewModel.viewType: + return new FirstPersonViewModel(model, updateStateCallback); + + case GlobeViewModel.viewType: + return new GlobeViewModel(model, updateStateCallback); + + case MapViewModel.viewType: + return new MapViewModel(model, updateStateCallback); + + case OrbitViewModel.viewType: + return new OrbitViewModel(model, updateStateCallback); + + case OrthographicViewModel.viewType: + return new OrthographicViewModel(model, updateStateCallback); + + default: + throw new Error(`no view supported for ${viewType}`); + } +} diff --git a/src/renderers/overlay.tsx b/src/renderers/overlay.tsx index 5509e79a..a2b96043 100644 --- a/src/renderers/overlay.tsx +++ b/src/renderers/overlay.tsx @@ -3,6 +3,7 @@ import React from "react"; import Map, { useControl } from "react-map-gl/maplibre"; import type { MapRendererProps, OverlayRendererProps } from "./types"; +import { isGlobeView } from "../util"; /** * DeckGLOverlay component that integrates deck.gl with react-map-gl @@ -28,7 +29,7 @@ const OverlayRenderer: React.FC = ( mapProps, ) => { // Remove maplibre-specific props before passing to DeckGL - const { mapStyle, customAttribution, initialViewState, ...deckProps } = + const { mapStyle, customAttribution, initialViewState, views, ...deckProps } = mapProps; return ( = ( mapStyle={mapStyle} attributionControl={{ customAttribution }} style={{ width: "100%", height: "100%" }} + {...(isGlobeView(views) && { projection: "globe" })} > = Pick< +type ViewOrViews = View | View[]; +export type MapRendererProps = Pick< DeckProps, | "getCursor" | "getTooltip" @@ -15,6 +15,7 @@ export type MapRendererProps = Pick< | "parameters" | "pickingRadius" | "useDevicePixels" + | "views" > & { mapStyle: string; customAttribution: string; diff --git a/src/util.ts b/src/util.ts index 647bc4c0..ec37c154 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,4 +1,9 @@ /** Check for null and undefined */ + +import { _GlobeView as GlobeView } from "@deck.gl/core"; + +import { MapRendererProps } from "./renderers"; + // https://stackoverflow.com/a/52097445 export function isDefined(value: T | undefined | null): value is T { return value !== undefined && value !== null; @@ -7,3 +12,8 @@ export function isDefined(value: T | undefined | null): value is T { export function makePolygon(pt1: number[], pt2: number[]) { return [pt1, [pt1[0], pt2[1]], pt2, [pt2[0], pt1[1]], pt1]; } + +export function isGlobeView(views: MapRendererProps["views"]) { + const firstView = Array.isArray(views) ? views[0] : views; + return firstView instanceof GlobeView; +} diff --git a/tests/test_map.py b/tests/test_map.py index 4dee1d30..4e6970b0 100644 --- a/tests/test_map.py +++ b/tests/test_map.py @@ -2,6 +2,7 @@ from traitlets import TraitError from lonboard import Map, ScatterplotLayer, SolidPolygonLayer +from lonboard.basemap import MaplibreBasemap def test_map_fails_with_unexpected_argument(): @@ -38,3 +39,13 @@ def allow_single_layer(): def test_map_basemap_non_url(): with pytest.raises(TraitError, match=r"expected to be a HTTP\(s\) URL"): _m = Map([], basemap_style="hello world") + + +def test_map_default_basemap(): + m = Map([]) + assert isinstance(m.basemap, MaplibreBasemap), ( + "Default basemap should be MaplibreBasemap" + ) + + assert m.basemap.mode == MaplibreBasemap().mode, "Should match default parameters" + assert m.basemap.style == MaplibreBasemap().style, "Should match default parameters" diff --git a/uv.lock b/uv.lock index 97924ca0..6b3e419c 100644 --- a/uv.lock +++ b/uv.lock @@ -7,6 +7,15 @@ resolution-markers = [ "python_full_version < '3.11'", ] +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + [[package]] name = "anyio" version = "4.9.0" @@ -1777,6 +1786,7 @@ dependencies = [ { name = "ipywidgets" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pydantic" }, { name = "pyproj" }, { name = "traitlets" }, { name = "typing-extensions", marker = "python_full_version < '3.12'" }, @@ -1845,6 +1855,7 @@ requires-dist = [ { name = "movingpandas", marker = "extra == 'movingpandas'", specifier = ">=0.17" }, { name = "numpy", specifier = ">=1.14" }, { name = "pandas", marker = "extra == 'geopandas'", specifier = ">=2" }, + { name = "pydantic", specifier = ">=2.12.2" }, { name = "pyogrio", marker = "extra == 'cli'", specifier = ">=0.8" }, { name = "pyproj", specifier = ">=3.3" }, { name = "shapely", marker = "extra == 'cli'", specifier = ">=2" }, @@ -2850,6 +2861,135 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, ] +[[package]] +name = "pydantic" +version = "2.12.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/35/d319ed522433215526689bad428a94058b6dd12190ce7ddd78618ac14b28/pydantic-2.12.2.tar.gz", hash = "sha256:7b8fa15b831a4bbde9d5b84028641ac3080a4ca2cbd4a621a661687e741624fd", size = 816358, upload-time = "2025-10-14T15:02:21.842Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/98/468cb649f208a6f1279448e6e5247b37ae79cf5e4041186f1e2ef3d16345/pydantic-2.12.2-py3-none-any.whl", hash = "sha256:25ff718ee909acd82f1ff9b1a4acfd781bb23ab3739adaa7144f19a6a4e231ae", size = 460628, upload-time = "2025-10-14T15:02:19.623Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5", size = 457557, upload-time = "2025-10-14T10:23:47.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/3d/9b8ca77b0f76fcdbf8bc6b72474e264283f461284ca84ac3fde570c6c49a/pydantic_core-2.41.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2442d9a4d38f3411f22eb9dd0912b7cbf4b7d5b6c92c4173b75d3e1ccd84e36e", size = 2111197, upload-time = "2025-10-14T10:19:43.303Z" }, + { url = "https://files.pythonhosted.org/packages/59/92/b7b0fe6ed4781642232755cb7e56a86e2041e1292f16d9ae410a0ccee5ac/pydantic_core-2.41.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:30a9876226dda131a741afeab2702e2d127209bde3c65a2b8133f428bc5d006b", size = 1917909, upload-time = "2025-10-14T10:19:45.194Z" }, + { url = "https://files.pythonhosted.org/packages/52/8c/3eb872009274ffa4fb6a9585114e161aa1a0915af2896e2d441642929fe4/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d55bbac04711e2980645af68b97d445cdbcce70e5216de444a6c4b6943ebcccd", size = 1969905, upload-time = "2025-10-14T10:19:46.567Z" }, + { url = "https://files.pythonhosted.org/packages/f4/21/35adf4a753bcfaea22d925214a0c5b880792e3244731b3f3e6fec0d124f7/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1d778fb7849a42d0ee5927ab0f7453bf9f85eef8887a546ec87db5ddb178945", size = 2051938, upload-time = "2025-10-14T10:19:48.237Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d0/cdf7d126825e36d6e3f1eccf257da8954452934ede275a8f390eac775e89/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b65077a4693a98b90ec5ad8f203ad65802a1b9b6d4a7e48066925a7e1606706", size = 2250710, upload-time = "2025-10-14T10:19:49.619Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1c/af1e6fd5ea596327308f9c8d1654e1285cc3d8de0d584a3c9d7705bf8a7c/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62637c769dee16eddb7686bf421be48dfc2fae93832c25e25bc7242e698361ba", size = 2367445, upload-time = "2025-10-14T10:19:51.269Z" }, + { url = "https://files.pythonhosted.org/packages/d3/81/8cece29a6ef1b3a92f956ea6da6250d5b2d2e7e4d513dd3b4f0c7a83dfea/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfe3aa529c8f501babf6e502936b9e8d4698502b2cfab41e17a028d91b1ac7b", size = 2072875, upload-time = "2025-10-14T10:19:52.671Z" }, + { url = "https://files.pythonhosted.org/packages/e3/37/a6a579f5fc2cd4d5521284a0ab6a426cc6463a7b3897aeb95b12f1ba607b/pydantic_core-2.41.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca2322da745bf2eeb581fc9ea3bbb31147702163ccbcbf12a3bb630e4bf05e1d", size = 2191329, upload-time = "2025-10-14T10:19:54.214Z" }, + { url = "https://files.pythonhosted.org/packages/ae/03/505020dc5c54ec75ecba9f41119fd1e48f9e41e4629942494c4a8734ded1/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e8cd3577c796be7231dcf80badcf2e0835a46665eaafd8ace124d886bab4d700", size = 2151658, upload-time = "2025-10-14T10:19:55.843Z" }, + { url = "https://files.pythonhosted.org/packages/cb/5d/2c0d09fb53aa03bbd2a214d89ebfa6304be7df9ed86ee3dc7770257f41ee/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:1cae8851e174c83633f0833e90636832857297900133705ee158cf79d40f03e6", size = 2316777, upload-time = "2025-10-14T10:19:57.607Z" }, + { url = "https://files.pythonhosted.org/packages/ea/4b/c2c9c8f5e1f9c864b57d08539d9d3db160e00491c9f5ee90e1bfd905e644/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a26d950449aae348afe1ac8be5525a00ae4235309b729ad4d3399623125b43c9", size = 2320705, upload-time = "2025-10-14T10:19:59.016Z" }, + { url = "https://files.pythonhosted.org/packages/28/c3/a74c1c37f49c0a02c89c7340fafc0ba816b29bd495d1a31ce1bdeacc6085/pydantic_core-2.41.4-cp310-cp310-win32.whl", hash = "sha256:0cf2a1f599efe57fa0051312774280ee0f650e11152325e41dfd3018ef2c1b57", size = 1975464, upload-time = "2025-10-14T10:20:00.581Z" }, + { url = "https://files.pythonhosted.org/packages/d6/23/5dd5c1324ba80303368f7569e2e2e1a721c7d9eb16acb7eb7b7f85cb1be2/pydantic_core-2.41.4-cp310-cp310-win_amd64.whl", hash = "sha256:a8c2e340d7e454dc3340d3d2e8f23558ebe78c98aa8f68851b04dcb7bc37abdc", size = 2024497, upload-time = "2025-10-14T10:20:03.018Z" }, + { url = "https://files.pythonhosted.org/packages/62/4c/f6cbfa1e8efacd00b846764e8484fe173d25b8dab881e277a619177f3384/pydantic_core-2.41.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:28ff11666443a1a8cf2a044d6a545ebffa8382b5f7973f22c36109205e65dc80", size = 2109062, upload-time = "2025-10-14T10:20:04.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/f8/40b72d3868896bfcd410e1bd7e516e762d326201c48e5b4a06446f6cf9e8/pydantic_core-2.41.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61760c3925d4633290292bad462e0f737b840508b4f722247d8729684f6539ae", size = 1916301, upload-time = "2025-10-14T10:20:06.857Z" }, + { url = "https://files.pythonhosted.org/packages/94/4d/d203dce8bee7faeca791671c88519969d98d3b4e8f225da5b96dad226fc8/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eae547b7315d055b0de2ec3965643b0ab82ad0106a7ffd29615ee9f266a02827", size = 1968728, upload-time = "2025-10-14T10:20:08.353Z" }, + { url = "https://files.pythonhosted.org/packages/65/f5/6a66187775df87c24d526985b3a5d78d861580ca466fbd9d4d0e792fcf6c/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef9ee5471edd58d1fcce1c80ffc8783a650e3e3a193fe90d52e43bb4d87bff1f", size = 2050238, upload-time = "2025-10-14T10:20:09.766Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b9/78336345de97298cf53236b2f271912ce11f32c1e59de25a374ce12f9cce/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15dd504af121caaf2c95cb90c0ebf71603c53de98305621b94da0f967e572def", size = 2249424, upload-time = "2025-10-14T10:20:11.732Z" }, + { url = "https://files.pythonhosted.org/packages/99/bb/a4584888b70ee594c3d374a71af5075a68654d6c780369df269118af7402/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a926768ea49a8af4d36abd6a8968b8790f7f76dd7cbd5a4c180db2b4ac9a3a2", size = 2366047, upload-time = "2025-10-14T10:20:13.647Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8d/17fc5de9d6418e4d2ae8c675f905cdafdc59d3bf3bf9c946b7ab796a992a/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6916b9b7d134bff5440098a4deb80e4cb623e68974a87883299de9124126c2a8", size = 2071163, upload-time = "2025-10-14T10:20:15.307Z" }, + { url = "https://files.pythonhosted.org/packages/54/e7/03d2c5c0b8ed37a4617430db68ec5e7dbba66358b629cd69e11b4d564367/pydantic_core-2.41.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5cf90535979089df02e6f17ffd076f07237efa55b7343d98760bde8743c4b265", size = 2190585, upload-time = "2025-10-14T10:20:17.3Z" }, + { url = "https://files.pythonhosted.org/packages/be/fc/15d1c9fe5ad9266a5897d9b932b7f53d7e5cfc800573917a2c5d6eea56ec/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7533c76fa647fade2d7ec75ac5cc079ab3f34879626dae5689b27790a6cf5a5c", size = 2150109, upload-time = "2025-10-14T10:20:19.143Z" }, + { url = "https://files.pythonhosted.org/packages/26/ef/e735dd008808226c83ba56972566138665b71477ad580fa5a21f0851df48/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:37e516bca9264cbf29612539801ca3cd5d1be465f940417b002905e6ed79d38a", size = 2315078, upload-time = "2025-10-14T10:20:20.742Z" }, + { url = "https://files.pythonhosted.org/packages/90/00/806efdcf35ff2ac0f938362350cd9827b8afb116cc814b6b75cf23738c7c/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0c19cb355224037c83642429b8ce261ae108e1c5fbf5c028bac63c77b0f8646e", size = 2318737, upload-time = "2025-10-14T10:20:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/41/7e/6ac90673fe6cb36621a2283552897838c020db343fa86e513d3f563b196f/pydantic_core-2.41.4-cp311-cp311-win32.whl", hash = "sha256:09c2a60e55b357284b5f31f5ab275ba9f7f70b7525e18a132ec1f9160b4f1f03", size = 1974160, upload-time = "2025-10-14T10:20:23.817Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9d/7c5e24ee585c1f8b6356e1d11d40ab807ffde44d2db3b7dfd6d20b09720e/pydantic_core-2.41.4-cp311-cp311-win_amd64.whl", hash = "sha256:711156b6afb5cb1cb7c14a2cc2c4a8b4c717b69046f13c6b332d8a0a8f41ca3e", size = 2021883, upload-time = "2025-10-14T10:20:25.48Z" }, + { url = "https://files.pythonhosted.org/packages/33/90/5c172357460fc28b2871eb4a0fb3843b136b429c6fa827e4b588877bf115/pydantic_core-2.41.4-cp311-cp311-win_arm64.whl", hash = "sha256:6cb9cf7e761f4f8a8589a45e49ed3c0d92d1d696a45a6feaee8c904b26efc2db", size = 1968026, upload-time = "2025-10-14T10:20:27.039Z" }, + { url = "https://files.pythonhosted.org/packages/e9/81/d3b3e95929c4369d30b2a66a91db63c8ed0a98381ae55a45da2cd1cc1288/pydantic_core-2.41.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ab06d77e053d660a6faaf04894446df7b0a7e7aba70c2797465a0a1af00fc887", size = 2099043, upload-time = "2025-10-14T10:20:28.561Z" }, + { url = "https://files.pythonhosted.org/packages/58/da/46fdac49e6717e3a94fc9201403e08d9d61aa7a770fab6190b8740749047/pydantic_core-2.41.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c53ff33e603a9c1179a9364b0a24694f183717b2e0da2b5ad43c316c956901b2", size = 1910699, upload-time = "2025-10-14T10:20:30.217Z" }, + { url = "https://files.pythonhosted.org/packages/1e/63/4d948f1b9dd8e991a5a98b77dd66c74641f5f2e5225fee37994b2e07d391/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:304c54176af2c143bd181d82e77c15c41cbacea8872a2225dd37e6544dce9999", size = 1952121, upload-time = "2025-10-14T10:20:32.246Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a7/e5fc60a6f781fc634ecaa9ecc3c20171d238794cef69ae0af79ac11b89d7/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:025ba34a4cf4fb32f917d5d188ab5e702223d3ba603be4d8aca2f82bede432a4", size = 2041590, upload-time = "2025-10-14T10:20:34.332Z" }, + { url = "https://files.pythonhosted.org/packages/70/69/dce747b1d21d59e85af433428978a1893c6f8a7068fa2bb4a927fba7a5ff/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9f5f30c402ed58f90c70e12eff65547d3ab74685ffe8283c719e6bead8ef53f", size = 2219869, upload-time = "2025-10-14T10:20:35.965Z" }, + { url = "https://files.pythonhosted.org/packages/83/6a/c070e30e295403bf29c4df1cb781317b6a9bac7cd07b8d3acc94d501a63c/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd96e5d15385d301733113bcaa324c8bcf111275b7675a9c6e88bfb19fc05e3b", size = 2345169, upload-time = "2025-10-14T10:20:37.627Z" }, + { url = "https://files.pythonhosted.org/packages/f0/83/06d001f8043c336baea7fd202a9ac7ad71f87e1c55d8112c50b745c40324/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f348cbb44fae6e9653c1055db7e29de67ea6a9ca03a5fa2c2e11a47cff0e47", size = 2070165, upload-time = "2025-10-14T10:20:39.246Z" }, + { url = "https://files.pythonhosted.org/packages/14/0a/e567c2883588dd12bcbc110232d892cf385356f7c8a9910311ac997ab715/pydantic_core-2.41.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec22626a2d14620a83ca583c6f5a4080fa3155282718b6055c2ea48d3ef35970", size = 2189067, upload-time = "2025-10-14T10:20:41.015Z" }, + { url = "https://files.pythonhosted.org/packages/f4/1d/3d9fca34273ba03c9b1c5289f7618bc4bd09c3ad2289b5420481aa051a99/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a95d4590b1f1a43bf33ca6d647b990a88f4a3824a8c4572c708f0b45a5290ed", size = 2132997, upload-time = "2025-10-14T10:20:43.106Z" }, + { url = "https://files.pythonhosted.org/packages/52/70/d702ef7a6cd41a8afc61f3554922b3ed8d19dd54c3bd4bdbfe332e610827/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:f9672ab4d398e1b602feadcffcdd3af44d5f5e6ddc15bc7d15d376d47e8e19f8", size = 2307187, upload-time = "2025-10-14T10:20:44.849Z" }, + { url = "https://files.pythonhosted.org/packages/68/4c/c06be6e27545d08b802127914156f38d10ca287a9e8489342793de8aae3c/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:84d8854db5f55fead3b579f04bda9a36461dab0730c5d570e1526483e7bb8431", size = 2305204, upload-time = "2025-10-14T10:20:46.781Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e5/35ae4919bcd9f18603419e23c5eaf32750224a89d41a8df1a3704b69f77e/pydantic_core-2.41.4-cp312-cp312-win32.whl", hash = "sha256:9be1c01adb2ecc4e464392c36d17f97e9110fbbc906bcbe1c943b5b87a74aabd", size = 1972536, upload-time = "2025-10-14T10:20:48.39Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c2/49c5bb6d2a49eb2ee3647a93e3dae7080c6409a8a7558b075027644e879c/pydantic_core-2.41.4-cp312-cp312-win_amd64.whl", hash = "sha256:d682cf1d22bab22a5be08539dca3d1593488a99998f9f412137bc323179067ff", size = 2031132, upload-time = "2025-10-14T10:20:50.421Z" }, + { url = "https://files.pythonhosted.org/packages/06/23/936343dbcba6eec93f73e95eb346810fc732f71ba27967b287b66f7b7097/pydantic_core-2.41.4-cp312-cp312-win_arm64.whl", hash = "sha256:833eebfd75a26d17470b58768c1834dfc90141b7afc6eb0429c21fc5a21dcfb8", size = 1969483, upload-time = "2025-10-14T10:20:52.35Z" }, + { url = "https://files.pythonhosted.org/packages/13/d0/c20adabd181a029a970738dfe23710b52a31f1258f591874fcdec7359845/pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746", size = 2105688, upload-time = "2025-10-14T10:20:54.448Z" }, + { url = "https://files.pythonhosted.org/packages/00/b6/0ce5c03cec5ae94cca220dfecddc453c077d71363b98a4bbdb3c0b22c783/pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced", size = 1910807, upload-time = "2025-10-14T10:20:56.115Z" }, + { url = "https://files.pythonhosted.org/packages/68/3e/800d3d02c8beb0b5c069c870cbb83799d085debf43499c897bb4b4aaff0d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a", size = 1956669, upload-time = "2025-10-14T10:20:57.874Z" }, + { url = "https://files.pythonhosted.org/packages/60/a4/24271cc71a17f64589be49ab8bd0751f6a0a03046c690df60989f2f95c2c/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02", size = 2051629, upload-time = "2025-10-14T10:21:00.006Z" }, + { url = "https://files.pythonhosted.org/packages/68/de/45af3ca2f175d91b96bfb62e1f2d2f1f9f3b14a734afe0bfeff079f78181/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1", size = 2224049, upload-time = "2025-10-14T10:21:01.801Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/ae4e1ff84672bf869d0a77af24fd78387850e9497753c432875066b5d622/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2", size = 2342409, upload-time = "2025-10-14T10:21:03.556Z" }, + { url = "https://files.pythonhosted.org/packages/18/62/273dd70b0026a085c7b74b000394e1ef95719ea579c76ea2f0cc8893736d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84", size = 2069635, upload-time = "2025-10-14T10:21:05.385Z" }, + { url = "https://files.pythonhosted.org/packages/30/03/cf485fff699b4cdaea469bc481719d3e49f023241b4abb656f8d422189fc/pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d", size = 2194284, upload-time = "2025-10-14T10:21:07.122Z" }, + { url = "https://files.pythonhosted.org/packages/f9/7e/c8e713db32405dfd97211f2fc0a15d6bf8adb7640f3d18544c1f39526619/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d", size = 2137566, upload-time = "2025-10-14T10:21:08.981Z" }, + { url = "https://files.pythonhosted.org/packages/04/f7/db71fd4cdccc8b75990f79ccafbbd66757e19f6d5ee724a6252414483fb4/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2", size = 2316809, upload-time = "2025-10-14T10:21:10.805Z" }, + { url = "https://files.pythonhosted.org/packages/76/63/a54973ddb945f1bca56742b48b144d85c9fc22f819ddeb9f861c249d5464/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab", size = 2311119, upload-time = "2025-10-14T10:21:12.583Z" }, + { url = "https://files.pythonhosted.org/packages/f8/03/5d12891e93c19218af74843a27e32b94922195ded2386f7b55382f904d2f/pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c", size = 1981398, upload-time = "2025-10-14T10:21:14.584Z" }, + { url = "https://files.pythonhosted.org/packages/be/d8/fd0de71f39db91135b7a26996160de71c073d8635edfce8b3c3681be0d6d/pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4", size = 2030735, upload-time = "2025-10-14T10:21:16.432Z" }, + { url = "https://files.pythonhosted.org/packages/72/86/c99921c1cf6650023c08bfab6fe2d7057a5142628ef7ccfa9921f2dda1d5/pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564", size = 1973209, upload-time = "2025-10-14T10:21:18.213Z" }, + { url = "https://files.pythonhosted.org/packages/36/0d/b5706cacb70a8414396efdda3d72ae0542e050b591119e458e2490baf035/pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4", size = 1877324, upload-time = "2025-10-14T10:21:20.363Z" }, + { url = "https://files.pythonhosted.org/packages/de/2d/cba1fa02cfdea72dfb3a9babb067c83b9dff0bbcb198368e000a6b756ea7/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2", size = 1884515, upload-time = "2025-10-14T10:21:22.339Z" }, + { url = "https://files.pythonhosted.org/packages/07/ea/3df927c4384ed9b503c9cc2d076cf983b4f2adb0c754578dfb1245c51e46/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf", size = 2042819, upload-time = "2025-10-14T10:21:26.683Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ee/df8e871f07074250270a3b1b82aad4cd0026b588acd5d7d3eb2fcb1471a3/pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2", size = 1995866, upload-time = "2025-10-14T10:21:28.951Z" }, + { url = "https://files.pythonhosted.org/packages/fc/de/b20f4ab954d6d399499c33ec4fafc46d9551e11dc1858fb7f5dca0748ceb/pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89", size = 1970034, upload-time = "2025-10-14T10:21:30.869Z" }, + { url = "https://files.pythonhosted.org/packages/54/28/d3325da57d413b9819365546eb9a6e8b7cbd9373d9380efd5f74326143e6/pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1", size = 2102022, upload-time = "2025-10-14T10:21:32.809Z" }, + { url = "https://files.pythonhosted.org/packages/9e/24/b58a1bc0d834bf1acc4361e61233ee217169a42efbdc15a60296e13ce438/pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac", size = 1905495, upload-time = "2025-10-14T10:21:34.812Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a4/71f759cc41b7043e8ecdaab81b985a9b6cad7cec077e0b92cff8b71ecf6b/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554", size = 1956131, upload-time = "2025-10-14T10:21:36.924Z" }, + { url = "https://files.pythonhosted.org/packages/b0/64/1e79ac7aa51f1eec7c4cda8cbe456d5d09f05fdd68b32776d72168d54275/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e", size = 2052236, upload-time = "2025-10-14T10:21:38.927Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e3/a3ffc363bd4287b80f1d43dc1c28ba64831f8dfc237d6fec8f2661138d48/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616", size = 2223573, upload-time = "2025-10-14T10:21:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/28/27/78814089b4d2e684a9088ede3790763c64693c3d1408ddc0a248bc789126/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af", size = 2342467, upload-time = "2025-10-14T10:21:44.018Z" }, + { url = "https://files.pythonhosted.org/packages/92/97/4de0e2a1159cb85ad737e03306717637842c88c7fd6d97973172fb183149/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12", size = 2063754, upload-time = "2025-10-14T10:21:46.466Z" }, + { url = "https://files.pythonhosted.org/packages/0f/50/8cb90ce4b9efcf7ae78130afeb99fd1c86125ccdf9906ef64b9d42f37c25/pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d", size = 2196754, upload-time = "2025-10-14T10:21:48.486Z" }, + { url = "https://files.pythonhosted.org/packages/34/3b/ccdc77af9cd5082723574a1cc1bcae7a6acacc829d7c0a06201f7886a109/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad", size = 2137115, upload-time = "2025-10-14T10:21:50.63Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ba/e7c7a02651a8f7c52dc2cff2b64a30c313e3b57c7d93703cecea76c09b71/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a", size = 2317400, upload-time = "2025-10-14T10:21:52.959Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ba/6c533a4ee8aec6b812c643c49bb3bd88d3f01e3cebe451bb85512d37f00f/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025", size = 2312070, upload-time = "2025-10-14T10:21:55.419Z" }, + { url = "https://files.pythonhosted.org/packages/22/ae/f10524fcc0ab8d7f96cf9a74c880243576fd3e72bd8ce4f81e43d22bcab7/pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e", size = 1982277, upload-time = "2025-10-14T10:21:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/b4/dc/e5aa27aea1ad4638f0c3fb41132f7eb583bd7420ee63204e2d4333a3bbf9/pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894", size = 2024608, upload-time = "2025-10-14T10:21:59.557Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/51d89cc2612bd147198e120a13f150afbf0bcb4615cddb049ab10b81b79e/pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d", size = 1967614, upload-time = "2025-10-14T10:22:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c2/472f2e31b95eff099961fa050c376ab7156a81da194f9edb9f710f68787b/pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da", size = 1876904, upload-time = "2025-10-14T10:22:04.062Z" }, + { url = "https://files.pythonhosted.org/packages/4a/07/ea8eeb91173807ecdae4f4a5f4b150a520085b35454350fc219ba79e66a3/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e", size = 1882538, upload-time = "2025-10-14T10:22:06.39Z" }, + { url = "https://files.pythonhosted.org/packages/1e/29/b53a9ca6cd366bfc928823679c6a76c7a4c69f8201c0ba7903ad18ebae2f/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa", size = 2041183, upload-time = "2025-10-14T10:22:08.812Z" }, + { url = "https://files.pythonhosted.org/packages/c7/3d/f8c1a371ceebcaf94d6dd2d77c6cf4b1c078e13a5837aee83f760b4f7cfd/pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d", size = 1993542, upload-time = "2025-10-14T10:22:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0", size = 1973897, upload-time = "2025-10-14T10:22:13.444Z" }, + { url = "https://files.pythonhosted.org/packages/b0/12/5ba58daa7f453454464f92b3ca7b9d7c657d8641c48e370c3ebc9a82dd78/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:a1b2cfec3879afb742a7b0bcfa53e4f22ba96571c9e54d6a3afe1052d17d843b", size = 2122139, upload-time = "2025-10-14T10:22:47.288Z" }, + { url = "https://files.pythonhosted.org/packages/21/fb/6860126a77725c3108baecd10fd3d75fec25191d6381b6eb2ac660228eac/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:d175600d975b7c244af6eb9c9041f10059f20b8bbffec9e33fdd5ee3f67cdc42", size = 1936674, upload-time = "2025-10-14T10:22:49.555Z" }, + { url = "https://files.pythonhosted.org/packages/de/be/57dcaa3ed595d81f8757e2b44a38240ac5d37628bce25fb20d02c7018776/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f184d657fa4947ae5ec9c47bd7e917730fa1cbb78195037e32dcbab50aca5ee", size = 1956398, upload-time = "2025-10-14T10:22:52.19Z" }, + { url = "https://files.pythonhosted.org/packages/2f/1d/679a344fadb9695f1a6a294d739fbd21d71fa023286daeea8c0ed49e7c2b/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed810568aeffed3edc78910af32af911c835cc39ebbfacd1f0ab5dd53028e5c", size = 2138674, upload-time = "2025-10-14T10:22:54.499Z" }, + { url = "https://files.pythonhosted.org/packages/c4/48/ae937e5a831b7c0dc646b2ef788c27cd003894882415300ed21927c21efa/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:4f5d640aeebb438517150fdeec097739614421900e4a08db4a3ef38898798537", size = 2112087, upload-time = "2025-10-14T10:22:56.818Z" }, + { url = "https://files.pythonhosted.org/packages/5e/db/6db8073e3d32dae017da7e0d16a9ecb897d0a4d92e00634916e486097961/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:4a9ab037b71927babc6d9e7fc01aea9e66dc2a4a34dff06ef0724a4049629f94", size = 1920387, upload-time = "2025-10-14T10:22:59.342Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c1/dd3542d072fcc336030d66834872f0328727e3b8de289c662faa04aa270e/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4dab9484ec605c3016df9ad4fd4f9a390bc5d816a3b10c6550f8424bb80b18c", size = 1951495, upload-time = "2025-10-14T10:23:02.089Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c6/db8d13a1f8ab3f1eb08c88bd00fd62d44311e3456d1e85c0e59e0a0376e7/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8a5028425820731d8c6c098ab642d7b8b999758e24acae03ed38a66eca8335", size = 2139008, upload-time = "2025-10-14T10:23:04.539Z" }, + { url = "https://files.pythonhosted.org/packages/5d/d4/912e976a2dd0b49f31c98a060ca90b353f3b73ee3ea2fd0030412f6ac5ec/pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1e5ab4fc177dd41536b3c32b2ea11380dd3d4619a385860621478ac2d25ceb00", size = 2106739, upload-time = "2025-10-14T10:23:06.934Z" }, + { url = "https://files.pythonhosted.org/packages/71/f0/66ec5a626c81eba326072d6ee2b127f8c139543f1bf609b4842978d37833/pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:3d88d0054d3fa11ce936184896bed3c1c5441d6fa483b498fac6a5d0dd6f64a9", size = 1932549, upload-time = "2025-10-14T10:23:09.24Z" }, + { url = "https://files.pythonhosted.org/packages/c4/af/625626278ca801ea0a658c2dcf290dc9f21bb383098e99e7c6a029fccfc0/pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b2a054a8725f05b4b6503357e0ac1c4e8234ad3b0c2ac130d6ffc66f0e170e2", size = 2135093, upload-time = "2025-10-14T10:23:11.626Z" }, + { url = "https://files.pythonhosted.org/packages/20/f6/2fba049f54e0f4975fef66be654c597a1d005320fa141863699180c7697d/pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0d9db5a161c99375a0c68c058e227bee1d89303300802601d76a3d01f74e258", size = 2187971, upload-time = "2025-10-14T10:23:14.437Z" }, + { url = "https://files.pythonhosted.org/packages/0e/80/65ab839a2dfcd3b949202f9d920c34f9de5a537c3646662bdf2f7d999680/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:6273ea2c8ffdac7b7fda2653c49682db815aebf4a89243a6feccf5e36c18c347", size = 2147939, upload-time = "2025-10-14T10:23:16.831Z" }, + { url = "https://files.pythonhosted.org/packages/44/58/627565d3d182ce6dfda18b8e1c841eede3629d59c9d7cbc1e12a03aeb328/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:4c973add636efc61de22530b2ef83a65f39b6d6f656df97f678720e20de26caa", size = 2311400, upload-time = "2025-10-14T10:23:19.234Z" }, + { url = "https://files.pythonhosted.org/packages/24/06/8a84711162ad5a5f19a88cead37cca81b4b1f294f46260ef7334ae4f24d3/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b69d1973354758007f46cf2d44a4f3d0933f10b6dc9bf15cf1356e037f6f731a", size = 2316840, upload-time = "2025-10-14T10:23:21.738Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8b/b7bb512a4682a2f7fbfae152a755d37351743900226d29bd953aaf870eaa/pydantic_core-2.41.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3619320641fd212aaf5997b6ca505e97540b7e16418f4a241f44cdf108ffb50d", size = 2149135, upload-time = "2025-10-14T10:23:24.379Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7d/138e902ed6399b866f7cfe4435d22445e16fff888a1c00560d9dc79a780f/pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:491535d45cd7ad7e4a2af4a5169b0d07bebf1adfd164b0368da8aa41e19907a5", size = 2104721, upload-time = "2025-10-14T10:23:26.906Z" }, + { url = "https://files.pythonhosted.org/packages/47/13/0525623cf94627f7b53b4c2034c81edc8491cbfc7c28d5447fa318791479/pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:54d86c0cada6aba4ec4c047d0e348cbad7063b87ae0f005d9f8c9ad04d4a92a2", size = 1931608, upload-time = "2025-10-14T10:23:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f9/744bc98137d6ef0a233f808bfc9b18cf94624bf30836a18d3b05d08bf418/pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca1124aced216b2500dc2609eade086d718e8249cb9696660ab447d50a758bd", size = 2132986, upload-time = "2025-10-14T10:23:32.057Z" }, + { url = "https://files.pythonhosted.org/packages/17/c8/629e88920171173f6049386cc71f893dff03209a9ef32b4d2f7e7c264bcf/pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c9024169becccf0cb470ada03ee578d7348c119a0d42af3dcf9eda96e3a247c", size = 2187516, upload-time = "2025-10-14T10:23:34.871Z" }, + { url = "https://files.pythonhosted.org/packages/2e/0f/4f2734688d98488782218ca61bcc118329bf5de05bb7fe3adc7dd79b0b86/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:26895a4268ae5a2849269f4991cdc97236e4b9c010e51137becf25182daac405", size = 2146146, upload-time = "2025-10-14T10:23:37.342Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f2/ab385dbd94a052c62224b99cf99002eee99dbec40e10006c78575aead256/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:ca4df25762cf71308c446e33c9b1fdca2923a3f13de616e2a949f38bf21ff5a8", size = 2311296, upload-time = "2025-10-14T10:23:40.145Z" }, + { url = "https://files.pythonhosted.org/packages/fc/8e/e4f12afe1beeb9823bba5375f8f258df0cc61b056b0195fb1cf9f62a1a58/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:5a28fcedd762349519276c36634e71853b4541079cab4acaaac60c4421827308", size = 2315386, upload-time = "2025-10-14T10:23:42.624Z" }, + { url = "https://files.pythonhosted.org/packages/48/f7/925f65d930802e3ea2eb4d5afa4cb8730c8dc0d2cb89a59dc4ed2fcb2d74/pydantic_core-2.41.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c173ddcd86afd2535e2b695217e82191580663a1d1928239f877f5a1649ef39f", size = 2147775, upload-time = "2025-10-14T10:23:45.406Z" }, +] + [[package]] name = "pygments" version = "2.19.1" @@ -3598,11 +3738,23 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.6.3" +version = "4.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/42/56/cfaa7a5281734dadc842f3a22e50447c675a1c5a5b9f6ad8a07b467bffe7/typing_extensions-4.6.3.tar.gz", hash = "sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5", size = 65757, upload-time = "2023-06-01T23:55:36.332Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/86/d9b1518d8e75b346a33eb59fa31bdbbee11459a7e2cc5be502fa779e96c5/typing_extensions-4.6.3-py3-none-any.whl", hash = "sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26", size = 31329, upload-time = "2023-06-01T23:55:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] [[package]]