diff --git a/README.md b/README.md index 3779cc8..88562c8 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Python wrapper for the [sits](https://github.com/e-sensing/sits) R package. -## ๐Ÿ“ฆ Installation +## Installation To install `pysits` with pip: @@ -18,7 +18,7 @@ pip install git+https://github.com/e-sensing/pysits.git > To use `pysits`, you must have [sits](https://github.com/e-sensing/sits) installed. For more information, refer to the [official sits documentation](https://e-sensing.github.io/sitsbook/setup.html). -## ๐Ÿ›  Development setup (for contributors) +## Development setup (for contributors) To set up a local development environment: @@ -42,7 +42,7 @@ source .venv/bin/activate # or .venv\Scripts\activate on Windows uv pip install -e ".[dev]" ``` -### ๐Ÿ” Run tests +### Run tests We use `pytest` for testing: @@ -50,7 +50,7 @@ We use `pytest` for testing: pytest ``` -### ๐Ÿงน Code formatting +### Code formatting To keep the codebase clean and consistent we use [ruff](https://github.com/astral-sh/ruff): @@ -58,7 +58,7 @@ To keep the codebase clean and consistent we use [ruff](https://github.com/astra ruff format . ``` -### ๐Ÿงช Linting +### Linting We use [ruff](https://github.com/astral-sh/ruff) for static analysis: @@ -68,11 +68,11 @@ ruff check . > The `examples/` directory is excluded from linting. -## ๐Ÿ“š Learn more +## Learn more To learn all about `sits`, including its concepts, API, and real-world examples, we recommend accessing the [official sits book](https://e-sensing.github.io/sitsbook/). The book provides examples in both R and Python. -## ๐Ÿค Contributing +## Contributing We welcome contributions! Please: @@ -80,6 +80,6 @@ We welcome contributions! Please: - Create a feature branch - Submit a pull request with a clear description -## ๐Ÿ“„ License +## License `pysits` is distributed under the GPL-2.0 license. See [LICENSE](./LICENSE) for more details. diff --git a/pyproject.toml b/pyproject.toml index e1967c9..bc61932 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,10 @@ xarray = [ "xarray>=2025.3.0", "dask>=2025.3.0", "rioxarray>=0.18.2", - "affine>=2.4.0" + "affine>=2.4.0", + "odc-stac>=0.5.3", + "pystac>=1.15.0", + "planetary-computer>=1.0.0" ] dev = [ diff --git a/pysits/__init__.py b/pysits/__init__.py index 6349c8a..e0294cd 100644 --- a/pysits/__init__.py +++ b/pysits/__init__.py @@ -27,7 +27,13 @@ sits_colors_set, sits_colors_show, ) -from .sits.config import sits_config, sits_config_show, sits_config_user_file +from .sits.config import ( + sits_config, + sits_config_show, + sits_config_user_file, + sits_config_value, + sits_parallel, +) from .sits.context import ( cerrado_2classes, point_mt_6bands, @@ -72,9 +78,10 @@ impute_mean, impute_mean_window, impute_median, - sits_impute, ) from .sits.ml import ( + sits_barlow_twins, + sits_contrastive_learning, sits_formula_linear, sits_formula_logref, sits_kfold_validate, @@ -82,8 +89,12 @@ sits_lighttae, sits_mlp, sits_model_export, + sits_pre_train, sits_resnet, sits_rfor, + sits_ssl_lejepa, + sits_ssl_mae, + sits_ssl_vicreg, sits_svm, sits_tae, sits_tempcnn, @@ -91,11 +102,12 @@ sits_xgboost, ) from .sits.segment import sits_segment, sits_slic, sits_snic -from .sits.tiles import sits_mgrs_to_roi, sits_roi_to_tiles, sits_tiles_to_roi +from .sits.tiles import sits_roi_to_tiles, sits_tiles_to_roi from .sits.ts import ( sits_cluster_clean, sits_cluster_dendro, sits_cluster_frequency, + sits_encode, sits_geo_dist, sits_get_class, sits_get_data, @@ -109,7 +121,6 @@ sits_reduce_imbalance, sits_sample, sits_sampling_design, - sits_sgolay, sits_show_prediction, sits_som_clean_samples, sits_som_evaluate_cluster, @@ -117,7 +128,6 @@ sits_stats, sits_stratified_sampling, sits_validate, - sits_whittaker, ) from .sits.tuning import sits_tuning, sits_tuning_hparams from .sits.utils import ( @@ -158,6 +168,8 @@ "sits_config", "sits_config_show", "sits_config_user_file", + "sits_config_value", + "sits_parallel", # Data management "sits_bands", "sits_timeline", @@ -190,16 +202,19 @@ "sits_model_export", "sits_formula_linear", "sits_formula_logref", + "sits_ssl_mae", + "sits_ssl_lejepa", + "sits_ssl_vicreg", + "sits_barlow_twins", + "sits_contrastive_learning", + "sits_pre_train", # Impute - "sits_impute", "impute_linear", "impute_mean", "impute_median", "impute_mean_window", # Time-series "sits_show_prediction", - "sits_sgolay", - "sits_whittaker", "sits_get_data", "sits_get_class", "sits_get_probs", @@ -219,8 +234,8 @@ "sits_reduce_imbalance", "sits_sampling_design", "sits_stratified_sampling", + "sits_encode", # Tiles - "sits_mgrs_to_roi", "sits_tiles_to_roi", "sits_roi_to_tiles", # Segments diff --git a/pysits/backend/functions.py b/pysits/backend/functions.py index 1874d43..70edb1a 100644 --- a/pysits/backend/functions.py +++ b/pysits/backend/functions.py @@ -17,7 +17,10 @@ """backend functions.""" -from pysits.backend.loaders import load_function_from_package +from pysits.backend.loaders import ( + load_function_from_package, + load_internal_function_from_package, +) # Base - plot r_fnc_plot = load_function_from_package("base::plot") @@ -40,6 +43,9 @@ # Base - class (base) r_fnc_class = load_function_from_package("base::class") +# Base - eval (base) +r_fnc_eval = load_function_from_package("base::eval") + # Base - as.data.frame (base) r_fnc_as_data_frame = load_function_from_package("base::as_data_frame") @@ -48,3 +54,6 @@ # Base - rownames (base) r_fnc_rownames = load_function_from_package("base::rownames") + +# sits - configuration (internal) +r_fnc_sits_conf = load_internal_function_from_package("sits:::.conf") diff --git a/pysits/backend/loaders.py b/pysits/backend/loaders.py index a485698..9c74f83 100644 --- a/pysits/backend/loaders.py +++ b/pysits/backend/loaders.py @@ -105,3 +105,42 @@ def load_function_from_package(name: str) -> Callable[..., Any]: # Return function return getattr(pkg, func_name) + + +def load_internal_function_from_package(name: str) -> Callable[..., Any]: + """Load an internal R function from a specified package. + + Internal functions are those not exported by a package. They are addressed + in R with the ``package:::function`` notation. + + Args: + name (str): The fully qualified name of the R function in the format + 'package:::function'. For example, 'sits:::.conf'. + + Returns: + Callable[..., Any]: A Python callable that wraps the R function. + The exact signature depends on the underlying R function. + + Raises: + ValueError: If the ``name`` doesn't follow the 'package:::function' format. + + PackageNotFoundError: If the specified R package is not installed. + + Examples: + >>> conf = load_internal_function_from_package('sits:::.conf') + >>> scale = conf('sources', 'BDC', 'collections', 'MOD13Q1-6.1') + """ + # Parse package and function + package_name, _, func_name = name.partition(":::") + + if not package_name or not func_name: + raise ValueError( + f"Invalid function name format: {name}. " + "Expected format: 'package:::function'" + ) + + # Import the package + importr(package_name, on_conflict="warn") + + # Return function + return rpy2_r_interface(name) diff --git a/pysits/conversions/common.py b/pysits/conversions/common.py index 2e6f687..a9d452a 100644 --- a/pysits/conversions/common.py +++ b/pysits/conversions/common.py @@ -25,8 +25,10 @@ from pandas import DataFrame as PandasDataFrame from rpy2.rinterface_lib.sexp import NULLType from rpy2.robjects import pandas2ri +from rpy2.robjects.language import LangVector from rpy2.robjects.robject import RObjectMixin +from pysits.backend.functions import r_fnc_eval from pysits.backend.pkgs import r_pkg_tibble from pysits.conversions.dsl.base import DSLObject from pysits.conversions.tibble import geopandas_to_tibble, pandas_to_tibble @@ -179,6 +181,22 @@ def convert_to_r(obj): raise TypeError(f"Cannot convert object of type {obj_type} to R format") +def eval_r_language(obj): + """Evaluate an unevaluated R expression. + + Args: + obj: The R object to evaluate. + + Returns: + The evaluated R object. Objects that are not R expressions are returned + unchanged. + """ + if isinstance(obj, LangVector): + return r_fnc_eval(obj) + + return obj + + def convert_to_python(obj, as_type="str"): """Convert an R object to a Python representation. @@ -206,6 +224,9 @@ def convert_to_python(obj, as_type="str"): def _convert(value, type_): result = [] + # R expressions must be evaluated to be converted + value = eval_r_language(value) + if isinstance(value, ro.ListVector): for k, v in value.items(): result.append( diff --git a/pysits/conversions/stac.py b/pysits/conversions/stac.py new file mode 100644 index 0000000..d3b6cb7 --- /dev/null +++ b/pysits/conversions/stac.py @@ -0,0 +1,233 @@ +# +# Copyright (C) 2025 sits developers. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, see . +# + +"""STAC conversions.""" + +from __future__ import annotations + +from urllib.parse import urlparse, urlunparse + +import pystac +from affine import Affine +from numpy import datetime64 +from odc.geo.geobox import GeoBox +from odc.geo.geom import CRS +from pandas import DataFrame as PandasDataFrame +from rasterio.warp import transform_bounds + +# +# Prefix used by GDAL to read files through HTTP +# +VSICURL_PREFIX = "/vsicurl/" + +# +# Host used by Microsoft Planetary Computer to store data +# +MPC_HOST_SUFFIX = "blob.core.windows.net" + + +# +# Grid operations +# +def files_geobox(files: PandasDataFrame) -> GeoBox: + """Create the geobox covering all files of a grid group. + + The geobox uses the native resolution and CRS of the files, so data is read + without resampling. + + Args: + files (PandasDataFrame): Files of a single grid group. + + Returns: + GeoBox: Geobox covering all files. + """ + xres = float(files["xres"].iloc[0]) + yres = float(files["yres"].iloc[0]) + + # Extent covering all files of the group + xmin = float(files["xmin"].min()) + xmax = float(files["xmax"].max()) + ymin = float(files["ymin"].min()) + ymax = float(files["ymax"].max()) + + # Shape (rounded, as files of a group share the same grid) + width = int(round((xmax - xmin) / xres)) + height = int(round((ymax - ymin) / yres)) + + return GeoBox( + shape=(height, width), + affine=Affine(xres, 0.0, xmin, 0.0, -yres, ymax), + crs=CRS(str(files["crs"].iloc[0])), + ) + + +def grid_group_name(files: PandasDataFrame, index: int) -> str: + """Create a name for a grid group. + + Args: + files (PandasDataFrame): Files of a single grid group. + + index (int): Position of the group, used when the CRS has no EPSG code. + + Returns: + str: Group name (e.g., ``epsg32720-10m``). + """ + crs = CRS(str(files["crs"].iloc[0])) + resolution = float(files["xres"].iloc[0]) + + try: + crs_name = f"epsg{crs.epsg}" if crs.epsg else f"grid{index}" + + except Exception: + crs_name = f"grid{index}" + + return f"{crs_name}-{resolution:g}m" + + +# +# STAC items +# +def stac_items_from_files(files: PandasDataFrame) -> list[pystac.Item]: + """Create STAC Items from cube files. + + One item is created for each ``(tile, date)``, with one asset per band. All + item properties come from the cube metadata: no file is read. + + Cubes can have more than one file for the same tile, date and band, as + collections may have multiple versions of an acquisition. Each version is + described as its own item, so all of them are used when data is read. + + Args: + files (PandasDataFrame): Files of a single grid group. + + Returns: + list[pystac.Item]: Items describing the files. + """ + items = [] + + # Files of the same tile, date and band are versions of an acquisition + files = files.copy() + files["version"] = files.groupby(["tile", "date", "band"]).cumcount() + + for (tile, date, version), group in files.groupby( + ["tile", "date", "version"], sort=True + ): + crs = str(group["crs"].iloc[0]) + assets = {} + + for _, file in group.iterrows(): + transform = Affine( + a=float(file["xres"]), + b=0.0, + c=float(file["xmin"]), + d=0.0, + e=-float(file["yres"]), + f=float(file["ymax"]), + ) + + # Create asset + asset = pystac.Asset( + href=str(file["path"]), + roles=["data"], + ) + + # Define extra fields + asset.extra_fields.update( + { + "proj:wkt2": crs, + "proj:shape": [int(file["nrows"]), int(file["ncols"])], + "proj:transform": [*list(transform)[:6], 0.0, 0.0, 1.0], + } + ) + + # Save band + assets[str(file["band"])] = asset + + # Footprint, in geographic coordinates (required by STAC) + bbox = list( + transform_bounds( + src_crs=crs, + dst_crs="EPSG:4326", + left=float(group["xmin"].min()), + bottom=float(group["ymin"].min()), + right=float(group["xmax"].max()), + top=float(group["ymax"].max()), + ) + ) + + # Create item + item = pystac.Item( + id=f"{tile}_{date}_{version}", + geometry={ + "type": "Polygon", + "coordinates": [ + [ + [bbox[0], bbox[1]], + [bbox[2], bbox[1]], + [bbox[2], bbox[3]], + [bbox[0], bbox[3]], + [bbox[0], bbox[1]], + ] + ], + }, + bbox=bbox, + datetime=datetime64(str(date)).astype("datetime64[s]").item(), + properties={"tile": tile}, + ) + + # Add assets + for band, asset in assets.items(): + item.add_asset(band, asset) + + # Save item + items.append(item) + + # Return! + return items + + +# +# Data access +# +def signature_file_url(url: str) -> str: + """Refresh the credentials of a file URL. + + Cubes store the credentials available when they were created. As these + credentials expire, they are refreshed before data is read. + + Args: + url (str): File URL. + + Returns: + str: URL with refreshed credentials, if required. + """ + prefix = VSICURL_PREFIX if url.startswith(VSICURL_PREFIX) else "" + address = url[len(prefix) :] + + parsed = urlparse(address) + + # Only data with expiring credentials is refreshed + if not parsed.netloc.endswith(MPC_HOST_SUFFIX) or not parsed.query: + return url + + try: + from planetary_computer import sign + + except ImportError: + return url + + return prefix + sign(urlunparse(parsed._replace(query=""))) diff --git a/pysits/conversions/tibble.py b/pysits/conversions/tibble.py index dcfe5b5..19e4d2b 100644 --- a/pysits/conversions/tibble.py +++ b/pysits/conversions/tibble.py @@ -17,28 +17,38 @@ """tibble conversions.""" +import io import warnings from collections.abc import Callable +import pyarrow as pa +import pyarrow.ipc from geopandas import GeoDataFrame as GeoPandasDataFrame from pandas import DataFrame as PandasDataFrame from pandas import to_datetime as pandas_to_datetime +from pandas.core.generic import NDFrame as PandasNDFrame from rpy2 import robjects +from rpy2.rinterface_lib.sexp import NULLType from rpy2.robjects import StrVector, pandas2ri +from rpy2.robjects import globalenv as rpy2_globalenv +from rpy2.robjects import r as rpy2_r_interface from rpy2.robjects.conversion import localconverter from rpy2.robjects.vectors import DataFrame as RDataFrame from shapely import wkt -from pysits.backend.functions import r_fnc_class -from pysits.backend.pkgs import r_pkg_base, r_pkg_sf -from pysits.models.frame import SITSFrameArray +from pysits.backend.functions import r_fnc_class, r_fnc_set_column +from pysits.backend.pkgs import r_pkg_base, r_pkg_sf, r_pkg_sits +from pysits.models.frame import NestedFrame # # Auxiliary functions # def _column_to_datetime(data: PandasDataFrame, colname: str) -> PandasDataFrame: - """Transform a columns from R to a valid datetime column in Python. + """Transform a column from R to a valid datetime column in Python. + + Handles both R integer date offsets (days since 1970-01-01) and + columns already converted to datetime by Arrow IPC. Args: data (pandas.DataFrame): Pandas Data Frame from an R tibble/data.frame. @@ -48,215 +58,377 @@ def _column_to_datetime(data: PandasDataFrame, colname: str) -> PandasDataFrame: Returns: pandas.DataFrame: Pandas data frame with ``colname`` as datetime. """ - # Convert if column is available if colname in data.columns: - data[colname] = pandas_to_datetime(data[colname], origin="1970-01-01", unit="D") + col = data[colname] + + if hasattr(col.dtype, "kind") and col.dtype.kind in ("i", "f"): + # R integer/float date offsets โ€” convert from days since epoch + data[colname] = pandas_to_datetime(col, origin="1970-01-01", unit="D") + + else: + # Already date/datetime or string โ€” just ensure datetime type + data[colname] = pandas_to_datetime(col) - # Return! return data def _sf_to_shapely(sf_object: RDataFrame) -> list: - """Transform a columns from R to a valid geometry column in Python. + """Transform R sf geometries to Shapely geometries via WKT. Args: - sf_object (rpy2.robjects.vectors.ListVector): R (tibble/data.frame) Data frame. + sf_object (rpy2.robjects.vectors.DataFrame): R sf data frame. Returns: list: List of Shapely geometries. """ - # Extract geometry and convert to WKT in R directly geom_wkt = r_pkg_sf.st_as_text(r_pkg_sf.st_geometry(sf_object)) - - # Convert R character vector to Python list of strings geom_wkt_py = list(geom_wkt) - # Convert each WKT string to a Shapely geometry return [wkt.loads(g) for g in geom_wkt_py] # -# Base conversion function +# Arrow IPC helpers # -def _tibble_to_pandas( +def _ensure_r_ipc_functions(): + """Define R-side IPC reader/writer/unnester functions once.""" + if "pysits_write_ipc_raw" in rpy2_globalenv: + return + + rpy2_r_interface(""" + pysits_write_ipc_raw <- function(df) { + tf <- tempfile(fileext = ".arrows") + + on.exit(unlink(tf)) + arrow::write_ipc_stream(df, tf) + + readBin(tf, "raw", file.info(tf)$size) + } + + pysits_read_ipc_raw <- function(raw_bytes) { + tf <- tempfile(fileext = ".arrows") + + on.exit(unlink(tf)) + writeBin(raw_bytes, tf) + + as.data.frame(arrow::read_ipc_stream(tf)) + } + + pysits_read_and_unnest <- function(raw_bytes, nested_cols) { + table <- pysits_read_ipc_raw(raw_bytes) + + purrr::map_dfr(seq_len(nrow(table)), function(idx) { + row_data <- table[idx,] + + for (col in nested_cols) { + row_nested <- row_data[[col]] + + # Handle arrow_list class + if (inherits(row_nested, "arrow_list")) { + row_nested <- lapply(row_nested, function(v) { + if (is.null(v)) { + return(NULL) + } + + tryCatch({ + parsed <- jsonlite::fromJSON(v) + setNames(as.character(parsed), names(parsed)) + }, error = function(e) { + NULL + }) + }) + + if (any(sapply(row_nested, is.null))) { + row_nested <- NULL + } + } else { + row_nested <- list(tidyr::unnest( + row_nested, + cols = dplyr::everything() + )) + } + + if (!is.null(row_nested)) { + row_data[[col]] <- NULL + + row_data <- tibble::tibble( + row_data, + !!col := row_nested + ) + } + } + row_data + }) + } + """) + + +def _dataframe_to_ipc_bytes(df: PandasDataFrame) -> bytes: + """Serialize a pandas DataFrame to Arrow IPC bytes. + + Args: + df (pandas.DataFrame): DataFrame to serialize. + + Returns: + bytes: Arrow IPC stream bytes. + """ + table = pa.Table.from_pandas(df) + sink = io.BytesIO() + + writer = pa.ipc.new_stream(sink, table.schema) + writer.write_table(table) + writer.close() + + return sink.getvalue() + + +def _ipc_bytes_to_dataframe(raw_bytes: bytes) -> PandasDataFrame: + """Deserialize Arrow IPC bytes to a pandas DataFrame. + + Args: + raw_bytes (bytes): Arrow IPC stream bytes. + + Returns: + pandas.DataFrame: Deserialized DataFrame. + """ + reader = pa.ipc.open_stream(raw_bytes) + + return reader.read_pandas() + + +def _named_vector_to_json(x: RDataFrame, colname: str) -> RDataFrame: + """Convert a named vector column to JSON strings. + + Args: + x (RDataFrame): R DataFrame containing a column with named vectors. + + colname (str): Name of the column containing named vectors. + + Returns: + RDataFrame: DataFrame with named vectors converted to JSON strings. + """ + rpy2_r_interface(f""" + named_vector_to_json <- function(x) {{ + vec_list <- lapply(x${colname}, function(v) {{ + if (is.null(names(v))) return(NULL) + class(v) <- NULL + json <- jsonlite::toJSON(as.list(setNames(as.character(v), names(v))), + auto_unbox=TRUE) + class(json) <- NULL + json + }}) + x${colname} <- vec_list + x + }} + """) + + return rpy2_globalenv["named_vector_to_json"](x) + + +# +# Core R-to-Python conversion +# +def _tibble_to_pandas( # noqa: PLR0912 data: RDataFrame, nested_columns: list | None = None, - table_processor: Callable[[PandasDataFrame], PandasDataFrame] | None = None, + table_processor: Callable | None = None, nested_processor: Callable[[PandasDataFrame], PandasDataFrame] | None = None, ) -> PandasDataFrame: - """Convert an R tibble containing nested data frames to a Pandas DataFrame. + """Convert an R tibble to a Pandas DataFrame using Arrow IPC. Args: - data (RDataFrame): An R tibble/data.frame object that contains - nested data frames. + data (RDataFrame): An R tibble/data.frame object. - nested_columns (list): List of column names that contain nested - data frames. + nested_columns (list | None): Column names containing nested data frames. - table_processor (Callable | None, optional): - A function to process the main table after conversion. The function should - take a pandas DataFrame as input and return a processed pandas DataFrame. - Defaults to None. + table_processor (Callable | None): Function to process the R data before + Arrow transfer. Receives and returns an RDataFrame. - nested_processor (Callable | None, optional): - A function to process each nested data frame after conversion. The function - should take a pandas DataFrame as input and return a processed pandas - DataFrame. Defaults to None. + nested_processor (Callable | None): Function to process each nested + pandas DataFrame after conversion. Returns: - PandasDataFrame: A pandas DataFrame where the nested columns are converted to - SITSFrameArray objects containing the processed nested - data frames. + PandasDataFrame: Converted DataFrame with nested columns as NestedFrame. """ + _ensure_r_ipc_functions() + # Check if the data is an SF object has_geometries = "sf" in r_fnc_class(data) - # Define shapely geometries and CRS shapely_crs = None shapely_geometries = None - # Handle SF objects if has_geometries: - # Convert geometry to Shapely geometries shapely_geometries = _sf_to_shapely(data) - - # Get CRS shapely_crs = r_pkg_sf.st_crs(data) - # Check if CRS is available if "NULL" not in r_fnc_class(shapely_crs): shapely_crs = shapely_crs.rx2("wkt")[0] - # Drop geometry column data = r_pkg_sf.st_drop_geometry(data) - # Convert columns definitions nested_columns = nested_columns if nested_columns else [] - # Extract columns from the data + # Extract and filter valid columns data_columns = r_pkg_base.colnames(data) - - # Remove invalid columns data_columns_valid = [] for data_column in data_columns: col = data.rx2(data_column) - # Remove invalid columns if r_fnc_class(col[0])[0] not in ["function", "NULL"]: data_columns_valid.append(data_column) - # Replace old data columns with the filtered one data_columns = data_columns_valid - # If user define nested columns, verify if they are available. + # Separate nested columns from regular columns if nested_columns: - # Filter available nested columns nested_columns = [v for v in nested_columns if v in data_columns] - # If selected columns are available, remove them from the data columns - # This allows us to handle them individually. if nested_columns: data_columns = list(set(data_columns).difference(nested_columns)) - # Select regular columns (using ``[]``) and convert to Pandas + # Select regular columns rdf_data = data.rx(StrVector(data_columns)) - rdf_data = pandas2ri.rpy2py(rdf_data) - # Handle nested columns if available + # Apply table processor on R side before transfer + if table_processor: + rdf_data = table_processor(rdf_data) + + # Transfer regular columns via IPC + r_write_fnc = rpy2_globalenv["pysits_write_ipc_raw"] + raw_bytes = bytes(r_write_fnc(rdf_data)) + result_df = _ipc_bytes_to_dataframe(raw_bytes) + + # Handle nested columns for nested_column in nested_columns: - # Select nested column (using ``[[]]``) nested_column_data = data.rx2(nested_column) - - # As it is a nested column, handle it as a list of ``tibble/data.frame`` nested_column_processed = [] for nested_row in nested_column_data: - # Convert to pandas - nested_row_df = pandas2ri.rpy2py(nested_row) + # Convert each nested tibble via IPC + try: + nested_raw = bytes(r_write_fnc(nested_row)) + nested_row_df = NestedFrame(_ipc_bytes_to_dataframe(nested_raw)) + + except Exception: + # Fallback to rpy2 conversion for non-standard nested data + nested_row_df = pandas2ri.rpy2py(nested_row) + + if isinstance(nested_row_df, PandasDataFrame): + nested_row_df = NestedFrame(nested_row_df) - # If a processor function is available, apply data to use if nested_processor and isinstance(nested_row_df, PandasDataFrame): - nested_row_df = nested_processor(nested_row_df) + nested_row_df = NestedFrame(nested_processor(nested_row_df)) - # Save nested_column_processed.append(nested_row_df) - # Convert column to SITS Array and save to the main data frame - rdf_data[nested_column] = SITSFrameArray(nested_column_processed) - - # If a processor function is available to the main table, use it - if table_processor: - rdf_data = table_processor(rdf_data) + result_df[nested_column] = nested_column_processed - # Transform dataframe to geodataframe + # Transform to GeoDataFrame if SF if shapely_geometries: - rdf_data = GeoPandasDataFrame( - rdf_data, geometry=shapely_geometries, crs=shapely_crs + result_df = GeoPandasDataFrame( + result_df, geometry=shapely_geometries, crs=shapely_crs ) - # Return! - return rdf_data + return result_df + + +# +# Core Python-to-R conversion +# +def _pandas_to_tibble( + instance: PandasDataFrame, nested_columns: list[str] | None = None +) -> RDataFrame: + """Convert a Pandas DataFrame to an R tibble using Arrow IPC. + + Args: + instance (PandasDataFrame): The Pandas DataFrame to convert. + + nested_columns (list[str] | None): Column names containing nested DataFrames. + + Returns: + RDataFrame: The converted R DataFrame (tibble). + """ + _ensure_r_ipc_functions() + + instance = instance.copy(deep=True) + + # Convert nested columns to dict-of-lists for Arrow serialization + if nested_columns: + nested_columns = [col for col in nested_columns if col in instance.columns] + + for nested_column in nested_columns: + instance[nested_column] = instance[nested_column].apply( + lambda arr: ( + arr.to_dict(orient="list") + if isinstance(arr, PandasNDFrame) + else arr + ) + ) + + # Serialize to IPC bytes + ipc_bytes = _dataframe_to_ipc_bytes(instance) + + # Transfer to R as raw vector + r_raw = robjects.vectors.ByteVector(ipc_bytes) + + if nested_columns: + # Read and unnest in R + r_unnest_fnc = rpy2_globalenv["pysits_read_and_unnest"] + + return r_unnest_fnc(r_raw, StrVector(nested_columns)) + + else: + # Simple read in R + r_read_fnc = rpy2_globalenv["pysits_read_ipc_raw"] + + return r_read_fnc(r_raw) # -# General function +# Public API โ€” General # def tibble_to_pandas(data: RDataFrame) -> PandasDataFrame: """Convert any tibble to Pandas DataFrame. Args: - data (rpy2.robjects.vectors.DataFrame): R (tibble/data.frame) Data frame. + data (RDataFrame): R (tibble/data.frame) Data frame. Returns: pandas.DataFrame: R Data Frame as Pandas. """ - # Define table processor def _table_processor(x): - """Table processor.""" - # Update date columns - x = _column_to_datetime(x, "start_date") - x = _column_to_datetime(x, "end_date") - + """Process date columns on R side (no-op; dates handled after IPC).""" return x - # Convert and return - return _tibble_to_pandas( - data=data, - table_processor=_table_processor, - ) + result = _tibble_to_pandas(data=data, table_processor=_table_processor) + result = _column_to_datetime(result, "start_date") + result = _column_to_datetime(result, "end_date") + + return result def tibble_nested_to_pandas( data: RDataFrame, nested_columns: list, - table_processor: Callable[[PandasDataFrame], PandasDataFrame] | None = None, + table_processor: Callable | None = None, nested_processor: Callable[[PandasDataFrame], PandasDataFrame] | None = None, ) -> PandasDataFrame: - """(Public) Convert an R tibble containing nested data frames to a Pandas DataFrame. + """Convert an R tibble with nested data frames to a Pandas DataFrame. Args: - data (RDataFrame): An R tibble/data.frame object that contains - nested data frames. + data (RDataFrame): An R tibble/data.frame with nested data frames. - nested_columns (list): List of column names that contain nested - data frames. + nested_columns (list): Column names containing nested data frames. - table_processor (Callable | None, optional): - A function to process the main table after conversion. The function should - take a pandas DataFrame as input and return a processed pandas DataFrame. - Defaults to None. + table_processor (Callable | None): Function to process the R data. - nested_processor (Callable | None, optional): - A function to process each nested data frame after conversion. The function - should take a pandas DataFrame as input and return a processed pandas - DataFrame. Defaults to None. + nested_processor (Callable | None): Function to process nested DataFrames. Returns: - PandasDataFrame: A pandas DataFrame where the nested columns are converted to - SITSFrameArray objects containing the processed nested - data frames. + PandasDataFrame: Converted DataFrame with nested columns. """ return _tibble_to_pandas( data=data, @@ -266,19 +438,79 @@ def tibble_nested_to_pandas( ) +def pandas_to_tibble(data: PandasDataFrame) -> RDataFrame: + """Convert a pandas DataFrame to an R DataFrame. + + Args: + data (pandas.DataFrame): The pandas DataFrame to convert to R. + + Returns: + rpy2.robjects.vectors.DataFrame: The converted R DataFrame object. + """ + with localconverter(robjects.default_converter + pandas2ri.converter): + return robjects.conversion.py2rpy(data) + + +def geopandas_to_tibble(data: GeoPandasDataFrame) -> RDataFrame: + """Convert a GeoPandas GeoDataFrame to an R sf object. + + Removes columns that contain embedded DataFrames (NestedFrame). + """ + data = GeoPandasDataFrame(data) + + if data.crs is None: + raise ValueError("GeoDataFrame must have a CRS") + + # Identify columns where no cell is a NestedFrame + safe_columns = [] + for col in data.columns: + if data[col].dtype == object: + first_valid = ( + data[col].dropna().iloc[0] if not data[col].dropna().empty else None + ) + + if isinstance(first_valid, PandasDataFrame): + continue + + safe_columns.append(col) + + dropped_columns = set(data.columns) - set(safe_columns) + if dropped_columns: + warnings.warn( + f"Warning: Dropping columns with embedded DataFrames: {dropped_columns}" + ) + + data_safe = data[safe_columns].copy() + + if isinstance(data, GeoPandasDataFrame): + geom_col = data.geometry.name + data_safe[geom_col] = data.geometry.to_wkt() + + with localconverter(robjects.default_converter + pandas2ri.converter): + r_df = robjects.conversion.py2rpy(data_safe) + + if isinstance(data, GeoPandasDataFrame): + r_df = r_pkg_sf.st_as_sf( + r_df, + wkt=robjects.StrVector([geom_col]), + crs=robjects.StrVector([data.crs.to_wkt()]), + ) + + return r_df + + # -# SITS conversions function +# Public API โ€” SITS-specific # def tibble_sits_to_pandas(data: RDataFrame) -> PandasDataFrame: - """Convert sits tibble to Pandas Data Frame. + """Convert a sits tibble to Pandas DataFrame. Args: - data (rpy2.robjects.vectors.DataFrame): R (tibble/data.frame) Data frame. + data (RDataFrame): R (tibble/data.frame) Data frame. Returns: - pandas.DataFrame: R Data Frame as Pandas. + pandas.DataFrame: Converted sits DataFrame. """ - # Define column order (from R) column_order = [ "longitude", "latitude", @@ -287,50 +519,91 @@ def tibble_sits_to_pandas(data: RDataFrame) -> PandasDataFrame: "label", "cube", "time_series", + "base_data", + "predicted", + "cluster", + "id_sample", + "id_neuron", + "count", ] - # Define nested columns - nested_columns = ["time_series", "predicted"] - - # Define table processor - def _table_processor(x): - """Table processor.""" - # Update date columns - x = _column_to_datetime(x, "start_date") - x = _column_to_datetime(x, "end_date") - - x_columns = list(filter(lambda y: y in x.columns, column_order)) - x_columns = x_columns + list(set(x.columns).difference(x_columns)) - - return x[x_columns] + nested_columns = [ + "time_series", + "base_data", + "predicted", + ] - # Define nested processor def _nested_processor(x): - """Nested processor.""" return _column_to_datetime(x, "Index") - # Convert and return - return _tibble_to_pandas( + result = _tibble_to_pandas( data=data, nested_columns=nested_columns, - table_processor=_table_processor, nested_processor=_nested_processor, ) + # Apply datetime conversions + result = _column_to_datetime(result, "start_date") + result = _column_to_datetime(result, "end_date") + + # Order columns + columns_available = [v for v in column_order if v in result.columns] + columns_available = columns_available + list( + set(result.columns).difference(columns_available) + ) + + return result[columns_available] + + +def pandas_sits_to_tibble(data: PandasDataFrame) -> RDataFrame: + """Convert a sits pandas DataFrame to R tibble. + + Args: + data (pandas.DataFrame): The pandas DataFrame to convert to R. + + Returns: + rpy2.robjects.vectors.DataFrame: The converted R DataFrame. + """ + nested_columns = [ + "time_series", + "base_data", + "predicted", + ] + + data_classes = [ + "sits", + "tbl_df", + "tbl", + "data.frame", + ] + + if "predicted" in data.columns: + data_classes.append("predicted") + + if "base_data" in data.columns: + data_classes.append("sits_base") + + if "id_sample" in data.columns and "id_neuron" in data.columns: + data_classes.append("som_clean_samples") + + data = _pandas_to_tibble(data, nested_columns) + data.rclass = StrVector(data_classes) + + return data + # -# Cube conversions function +# Public API โ€” Cube-specific # def tibble_cube_to_pandas(data: RDataFrame) -> PandasDataFrame: - """Convert sits tibble to Pandas Data Frame. + """Convert a sits cube tibble to Pandas DataFrame. Args: - data (rpy2.robjects.vectors.DataFrame): R (tibble/data.frame) Data frame. + data (RDataFrame): R (tibble/data.frame) Data frame. Returns: - pandas.DataFrame: R Data Frame as Pandas. + pandas.DataFrame: Converted cube DataFrame. """ - # Define column order (from R) column_order = [ "source", "collection", @@ -345,100 +618,81 @@ def tibble_cube_to_pandas(data: RDataFrame) -> PandasDataFrame: "labels", "file_info", "vector_info", + "base_info", ] - # Define nested columns - nested_columns = ["labels", "file_info", "vector_info"] - - # Define table processor - def _table_processor(x): - """Table processor.""" - columns_available = [v for v in column_order if v in x.columns] + nested_columns = [ + "file_info", + "vector_info", + ] - return x[columns_available] + def table_processor(x: RDataFrame) -> RDataFrame: + if "labels" in x.colnames: + labels = x.rx2("labels") + labels_has_names = all( + not isinstance(label.names, NULLType) for label in labels + ) - # Define nested processor - def _nested_processor(x): - """Nested processor.""" - x = _column_to_datetime(x, "date") - x = _column_to_datetime(x, "start_date") - x = _column_to_datetime(x, "end_date") + if labels_has_names: + return _named_vector_to_json(x, "labels") return x - # Convert and return - return _tibble_to_pandas( - data=data, - nested_columns=nested_columns, - table_processor=_table_processor, - nested_processor=_nested_processor, - ) + data_converted = _tibble_to_pandas(data, nested_columns, table_processor) + # Process base_info separately if it exists (recursive) + if "base_info" in data.colnames: + base_info = data.rx2("base_info") + base_info_converted = [] -# -# Pandas to R conversions -# -def pandas_to_tibble(data: PandasDataFrame) -> RDataFrame: - """Convert a pandas DataFrame to an R DataFrame object. + for i in range(len(base_info)): + if not isinstance(base_info[i], NULLType): + base_info_converted.append(tibble_cube_to_pandas(base_info[i])) - This function converts a pandas DataFrame to an R DataFrame using - rpy2's conversion infrastructure. It handles the conversion context - to ensure proper type mapping between Python and R objects. + else: + base_info_converted.append(None) - Args: - data (pandas.DataFrame): The pandas DataFrame to convert to R. + data_converted["base_info"] = base_info_converted - Returns: - rpy2.robjects.vectors.DataFrame: The converted R DataFrame object. - - Notes: - - The function uses rpy2's localconverter to ensure proper conversion context - - Handles both DataFrame and non-DataFrame inputs - - Preserves column names and data types where possible - - For non-DataFrame inputs, falls back to rpy2's default converter - """ - with localconverter(robjects.default_converter + pandas2ri.converter): - return robjects.conversion.py2rpy(data) + columns_available = [v for v in column_order if v in data_converted.columns] + return data_converted[columns_available] -def geopandas_to_tibble(data: GeoPandasDataFrame) -> RDataFrame: - """Convert pandas DataFrame or GeoDataFrame to R DataFrame or sf object. - Removes columns that contain embedded pandas DataFrames. - """ - data = GeoPandasDataFrame(data) +def pandas_cube_to_tibble(data: PandasDataFrame) -> RDataFrame: + """Convert a cube pandas DataFrame to R tibble. - if data.crs is None: - raise ValueError("GeoDataFrame must have a CRS") + Args: + data (pandas.DataFrame): The pandas DataFrame to convert to R. - # Identify columns where no cell is a DataFrame - safe_columns = [col for col in data.columns if not data[col].dtype.name == "sits"] + Returns: + rpy2.robjects.vectors.DataFrame: The converted R DataFrame. - # Warn if columns are dropped - dropped_columns = set(data.columns) - set(safe_columns) - if dropped_columns: - warnings.warn( - f"Warning: Dropping columns with embedded DataFrames: {dropped_columns}" + Raises: + ValueError: When ``data`` has no rows, as a data cube must describe at + least one tile. + """ + if data.empty: + raise ValueError( + "A data cube must have at least one tile, but no rows are available." ) - # Keep only safe columns - data_safe = data[safe_columns].copy() + nested_columns = [ + "labels", + "file_info", + "vector_info", + ] - # If GeoDataFrame, convert geometry to WKT and include geometry column - if isinstance(data, GeoPandasDataFrame): - geom_col = data.geometry.name - data_safe[geom_col] = data.geometry.to_wkt() + base_info = None + if "base_info" in data.columns: + base_info = pandas_cube_to_tibble(data.base_info) + data = data.drop(columns=["base_info"]) - # Convert to R DataFrame - with localconverter(robjects.default_converter + pandas2ri.converter): - r_df = robjects.conversion.py2rpy(data_safe) + data = _pandas_to_tibble(data, nested_columns) - # If GeoDataFrame, convert to sf - if isinstance(data, GeoPandasDataFrame): - r_df = r_pkg_sf.st_as_sf( - r_df, - wkt=robjects.StrVector([geom_col]), - crs=robjects.StrVector([data.crs.to_wkt()]), - ) + if base_info is not None: + data = r_fnc_set_column(data, "base_info", base_info) - return r_df + data.rclass = r_pkg_sits._cube_s3class(data) + + return data diff --git a/pysits/conversions/tibble_arrow.py b/pysits/conversions/tibble_arrow.py deleted file mode 100644 index a8d4209..0000000 --- a/pysits/conversions/tibble_arrow.py +++ /dev/null @@ -1,473 +0,0 @@ -# -# Copyright (C) 2025 sits developers. -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, see . -# - -"""Arrow conversions.""" - -import os -import tempfile -from collections.abc import Callable - -from pandas import DataFrame as PandasDataFrame -from pandas.core.generic import NDFrame as PandasNDFrame -from pyarrow import feather -from rpy2.rinterface_lib.sexp import NULLType -from rpy2.robjects import StrVector, pandas2ri -from rpy2.robjects import globalenv as rpy2_globalenv -from rpy2.robjects import r as rpy2_r_interface -from rpy2.robjects.vectors import DataFrame as RDataFrame - -from pysits.backend.functions import r_fnc_class, r_fnc_set_column -from pysits.backend.pkgs import r_pkg_arrow, r_pkg_base, r_pkg_sits - - -# -# Helper functions -# -def _load_arrow_table_reader_function() -> Callable[[str, list[str]], RDataFrame]: - """Load and return an R function for reading Arrow tables with nested columns. - - This function defines and returns an R function that reads a Feather file and - handles nested columns by unnesting them appropriately. The returned function - takes a file path and a list of nested column names as arguments. - - Returns: - Callable[[str, list[str]], RDataFrame]: An R function that takes a file path - and list of nested column names as input and returns an R DataFrame with - properly unnested columns. - """ - rpy2_r_interface(""" - load_arrow_table <- function(path, nested_cols) { - table <- arrow::read_feather(path) - - purrr::map_dfr(seq_len(nrow(table)), function(idx) { - row_data <- table[idx,] - - for (col in nested_cols) { - row_nested <- row_data[[col]] - - # Handle arrow_list class - if (inherits(row_nested, "arrow_list")) { - row_nested <- lapply(row_nested, function(v) { - if (is.null(v)) return(NULL) - # Try to parse as JSON first - tryCatch({ - parsed <- jsonlite::fromJSON(v) - setNames(as.character(parsed), names(parsed)) - }, error = function(e) { - # If JSON parsing fails, return NULL - NULL - }) - }) - # If any values in row_nested are NULL, set the whole - # thing to NULL - if (any(sapply(row_nested, is.null))) { - row_nested <- NULL - } - } else { - row_nested <- list(tidyr::unnest( - row_nested, - cols = dplyr::everything() - )) - } - - # Only create tibble if row_nested is not NULL - if (!is.null(row_nested)) { - row_data[[col]] <- NULL - row_data <- tibble::tibble( - row_data, - !!col := row_nested - ) - } - } - row_data - }) - } - """) - - return rpy2_globalenv["load_arrow_table"] - - -def _named_vector_to_json(x: RDataFrame, colname: str) -> RDataFrame: - """Convert a named vector to a JSON string. - - Args: - x (RDataFrame): R DataFrame containing a column with named vectors - - colname (str): Name of the column containing named vectors. - - Returns: - RDataFrame: DataFrame with named vectors converted to JSON strings - """ - # Define R code to convert named vector to JSON - rpy2_r_interface(f""" - named_vector_to_json <- function(x) {{ - vec_list <- lapply(x${colname}, function(v) {{ - if (is.null(names(v))) return(NULL) - class(v) <- NULL - json <- jsonlite::toJSON(as.list(setNames(as.character(v), names(v))), - auto_unbox=TRUE) - class(json) <- NULL - json - }}) - x${colname} <- vec_list - x - }} - """) - - # Call the R function and return result - return rpy2_globalenv["named_vector_to_json"](x) - - -def _tibble_to_pandas_arrow( - instance: RDataFrame, - nested_columns: list[str] | None = None, - table_processor: Callable[[RDataFrame], RDataFrame] | None = None, -) -> PandasDataFrame: - """Convert an R DataFrame (tibble) to a Pandas DataFrame using Arrow format. - - This function handles the conversion of R DataFrames to Pandas DataFrames by: - 1. Creating a temporary Feather file - 2. Filtering out invalid columns (functions and NULL values) - 3. Writing valid columns to Feather format - 4. Reading back into Pandas - 5. Converting any nested columns to Pandas DataFrames - - Args: - instance (RDataFrame): The R DataFrame (tibble) to convert. - - nested_columns (list[str] | None, optional): List of column names that - contain nested data. Defaults to None. - - Returns: - PandasDataFrame: The converted Pandas DataFrame. - """ - # Create a temporary file - tmp = tempfile.NamedTemporaryFile(suffix=".feather", delete=False) - tmp_path = tmp.name - tmp.close() - - # Check if instance is a empty - if instance.nrow == 0: - return pandas2ri.rpy2py(instance) - - # Extract columns from the data - data_columns = r_pkg_base.colnames(instance) - - # Remove invalid columns - data_columns_valid = [] - - for data_column in data_columns: - col = instance.rx2(data_column) - - # Remove invalid columns - if r_fnc_class(col[0])[0] not in ["function", "NULL"]: - data_columns_valid.append(data_column) - - # Select regular columns (using ``[]``) and convert to Pandas - rdf_data = instance.rx(StrVector(data_columns_valid)) - - # Process table - if table_processor: - rdf_data = table_processor(rdf_data) - - # Write to Feather format - r_pkg_arrow.write_feather(rdf_data, tmp_path) - - # Read from Feather format - df = feather.read_feather(tmp_path) - - # Convert nested columns to Pandas DataFrame - if nested_columns: - # Filter available columns - nested_columns = [col for col in nested_columns if col in df.columns] - - # Convert nested columns to Pandas DataFrame - for nested_column in nested_columns: - df[nested_column] = df[nested_column].apply( - lambda arr: PandasDataFrame.from_records(arr.tolist()) - ) - - # Remove temporary file - os.unlink(tmp_path) - - # Return value - return df - - -def _pandas_to_tibble_arrow( - instance: PandasDataFrame, nested_columns: list[str] | None = None -) -> RDataFrame: - """Convert a Pandas DataFrame to an R DataFrame (tibble) using Arrow format. - - This function handles the conversion of Pandas DataFrames to R DataFrames by: - 1. Creating a temporary Feather file - 2. Converting nested columns to a format suitable for R - 3. Writing the data to Feather format - 4. Reading back into R using a custom Arrow table reader - - Args: - instance (PandasDataFrame): The Pandas DataFrame to convert. - - nested_columns (list[str] | None, optional): List of column names that - contain nested data. Defaults to None. - - Returns: - RDataFrame: The converted R DataFrame (tibble). - """ - instance = instance.copy(deep=True) - - tmp = tempfile.NamedTemporaryFile(suffix=".feather", delete=False) - tmp_path = tmp.name - tmp.close() - - # Convert nested columns to R DataFrame - if nested_columns: - # Filter available columns - nested_columns = [col for col in nested_columns if col in instance.columns] - - # Convert nested columns to R DataFrame - for nested_column in nested_columns: - instance[nested_column] = instance[nested_column].apply( - lambda arr: ( - arr.to_dict(orient="list") - if isinstance(arr, PandasNDFrame) - else arr - ) - ) - - # Write to Feather - feather.write_feather(instance, tmp_path) - - # Load Arrow table reader function - load_arrow_table_fnc = _load_arrow_table_reader_function() - - # Read from Feather and unnest columns - return load_arrow_table_fnc(tmp_path, nested_columns) - - -# -# General conversions -# -def tibble_nested_to_pandas_arrow( - data: RDataFrame, - nested_columns: list[str], - table_processor: Callable[[RDataFrame], RDataFrame] | None = None, -) -> PandasDataFrame: - """Convert any tibble to Pandas DataFrame. - - Args: - data (rpy2.robjects.vectors.DataFrame): R (tibble/data.frame) Data frame. - - Returns: - pandas.DataFrame: R Data Frame as Pandas. - """ - return _tibble_to_pandas_arrow(data, nested_columns, table_processor) - - -def pandas_to_tibble_arrow( - data: PandasDataFrame, nested_columns: list[str] -) -> RDataFrame: - """Convert a pandas DataFrame to an R DataFrame object using Arrow. - - Args: - data (pandas.DataFrame): The pandas DataFrame to convert to R. - - Returns: - rpy2.robjects.vectors.DataFrame: The converted R DataFrame object. - """ - return _pandas_to_tibble_arrow(data, nested_columns) - - -# -# SITS conversions function -# -def tibble_sits_to_pandas_arrow(data: RDataFrame) -> PandasDataFrame: - """Convert sits tibble to Pandas DataFrame using Arrow. - - Args: - data (rpy2.robjects.vectors.DataFrame): R (tibble/data.frame) Data frame. - """ - # Define column order (from R) - column_order = [ - "longitude", - "latitude", - "start_date", - "end_date", - "label", - "cube", - "time_series", - "base_data", - "predicted", - "cluster", - "id_sample", - "id_neuron", - "count", - ] - - # Define nested columns - nested_columns = ["time_series", "base_data", "predicted"] - - # Convert to Pandas DataFrame - data_converted = tibble_nested_to_pandas_arrow(data, nested_columns) - - # Select columns - columns_available = [v for v in column_order if v in data_converted.columns] - - # Return value - return data_converted[columns_available] - - -def pandas_sits_to_tibble_arrow(data: PandasDataFrame) -> RDataFrame: - """Convert sits pandas DataFrame to R DataFrame object using Arrow. - - Args: - data (pandas.DataFrame): The pandas DataFrame to convert to R. - """ - # Define nested columns - nested_columns = ["time_series", "base_data", "predicted"] - - # Define data classes - data_classes = ["sits", "tbl_df", "tbl", "data.frame"] - - if "predicted" in data.columns: - data_classes.append("predicted") - - if "base_data" in data.columns: - data_classes.append("sits_base") - - if "id_sample" in data.columns and "id_neuron" in data.columns: - data_classes.append("som_clean_samples") - - # Convert to R DataFrame - data = pandas_to_tibble_arrow(data, nested_columns) - - # Set class - data.rclass = StrVector(data_classes) - - # Convert to R DataFrame - return data - - -# -# Cube conversions function -# -def tibble_cube_to_pandas_arrow(data: RDataFrame) -> PandasDataFrame: - """Convert sits tibble to Pandas DataFrame using Arrow. - - Args: - data (rpy2.robjects.vectors.DataFrame): R (tibble/data.frame) Data frame. - """ - # Define column order (from R) - column_order = [ - "source", - "collection", - "satellite", - "sensor", - "tile", - "xmin", - "xmax", - "ymin", - "ymax", - "crs", - "labels", - "file_info", - "vector_info", - "base_info", - ] - - # Define nested columns - nested_columns = ["file_info", "vector_info"] - - # Define table processor - def table_processor(x: RDataFrame) -> RDataFrame: - """Process table.""" - - # Process ``labels`` column - if "labels" in x.colnames: - # Get labels column - labels = x.rx2("labels") - - # Check if labels have names - labels_has_names = all( - not isinstance(label.names, NULLType) for label in labels - ) - - # If labels have names, convert to JSON - if labels_has_names: - x = _named_vector_to_json(x, "labels") - - return x - - # Convert to Pandas DataFrame - data_converted = tibble_nested_to_pandas_arrow( - data, nested_columns, table_processor - ) - - # Process base_info separately if it exists - if "base_info" in data.colnames: - base_info = data.rx2("base_info") - base_info_converted = [] - - # Convert each base_info item to a cube if it's not None - for i in range(len(base_info)): - if not isinstance(base_info[i], NULLType): - # Convert the base_info item to a cube - base_info_converted.append(tibble_cube_to_pandas_arrow(base_info[i])) - else: - base_info_converted.append(None) - - # Add converted base_info to the DataFrame - data_converted["base_info"] = base_info_converted - - # Select columns - columns_available = [v for v in column_order if v in data_converted.columns] - - # Return value - return data_converted[columns_available] - - -def pandas_cube_to_tibble_arrow(data: PandasDataFrame) -> RDataFrame: - """Convert sits pandas DataFrame to R DataFrame object using Arrow. - - Args: - data (pandas.DataFrame): The pandas DataFrame to convert to R. - """ - # Define nested columns - nested_columns = ["labels", "file_info", "vector_info"] - - # Handle base_info separately if it exists - base_info = None - - if "base_info" in data.columns: - # Convert base_info to R DataFrame - base_info = pandas_cube_to_tibble_arrow(data.base_info) - - # Drop base_info from data - data = data.drop(columns=["base_info"]) - - # Convert to R DataFrame - data = pandas_to_tibble_arrow(data, nested_columns) - - # Add base_info back if it exists - if base_info is not None: - data = r_fnc_set_column(data, "base_info", base_info) - - # Set class - data.rclass = r_pkg_sits._cube_s3class(data) - - # Return value - return data diff --git a/pysits/conversions/xarray.py b/pysits/conversions/xarray.py index c078a81..57cb6a0 100644 --- a/pysits/conversions/xarray.py +++ b/pysits/conversions/xarray.py @@ -19,189 +19,681 @@ from __future__ import annotations -import dask.array as dask_array import numpy as np import rioxarray as xrio import xarray as xr -from affine import Affine +from odc.stac import load as odc_load +from pandas import DataFrame as PandasDataFrame from pandas import concat as pandas_concat -from pandas import to_datetime as pandas_to_datetime +from pysits.conversions.stac import ( + files_geobox, + grid_group_name, + signature_file_url, + stac_items_from_files, +) from pysits.models.data.base import SITSData +from pysits.models.data.cube import SITSCubeModel +from pysits.sits.config import sits_config_value +from pysits.sits.data import sits_labels + +# +# Data types used by sits, and their numpy equivalents +# +DATA_TYPES = { + "INT1U": "uint8", + "INT1S": "int8", + "INT2U": "uint16", + "INT2S": "int16", + "INT4U": "uint32", + "INT4S": "int32", + "FLT4S": "float32", + "FLT8S": "float64", +} + +# +# Default chunks used to read data cubes +# +DEFAULT_CHUNKS = {"x": 512, "y": 512} + +# +# Name of the variable used in nodes of a data tree +# +TREE_VARIABLE = "cube" # -# Auxiliary functions +# Cube files # -def _xarray_load_raster( - raster_path: str, crs: str, shape: tuple[int, int], transform: float -) -> xr.Dataset: - """Load raster with rio-xarray. +def cube_files(cube: SITSCubeModel) -> PandasDataFrame: + """Merge the ``file_info`` of all cube rows, keeping tile metadata. Args: - raster_path (str): Complete path to the raster. + cube (SITSCubeModel): Data cube. - crs (str): Dataset CRS. + Returns: + PandasDataFrame: All cube files in a single data frame. + """ + files = [] + + for _, cube_row in cube.iterrows(): + file_info = cube_row["file_info"].copy() + + file_info["crs"] = str(cube_row["crs"]) + file_info["tile"] = str(cube_row["tile"]) - shape (Tuple[int, int]): Dataset shape. + files.append(file_info) - transform (float): Project transform. + # Return! + return pandas_concat(files, ignore_index=True) + + +def cube_grid_groups( + cube: SITSCubeModel, bands: list[str] | None = None +) -> dict[tuple, PandasDataFrame]: + """Group cube files by grid. + + Files are grouped by CRS and resolution. Files of a group share a grid, and + can be read together without resampling. + + Args: + cube (SITSCubeModel): Data cube. + + bands (list[str]): Bands to use. When ``None``, all bands are used. Returns: - xr.Dataset: Raster loaded as Dataset + dict[tuple, PandasDataFrame]: Files indexed by ``(crs, xres, yres)``. + + Raises: + ValueError: When a band is not available in the cube. """ - da = xrio.open_rasterio(raster_path, masked=True) - da = da.squeeze("band", drop=True) - - return da.rio.reproject( - dst_crs=crs, - shape=shape, - transform=transform, - resampling=0, # 0 = Nearest - ) + files = cube_files(cube) + + # Select bands + if bands is not None: + # Get bands available + available = sorted(files["band"].unique()) + + # Get bands not available + missing_bands = [band for band in bands if band not in available] + + # If user selected any band not available, error + if missing_bands: + raise ValueError( + f"Bands not available in the cube: {', '.join(missing_bands)}. " + f"Available bands: {', '.join(available)}" + ) + + # Select file of bands + files = files[files["band"].isin(bands)] + + # Group by grid + return { + key: group.reset_index(drop=True) + for key, group in files.groupby(["crs", "xres", "yres"], sort=True) + } # -# SITS conversions function +# Band configuration # -def pandas_sits_as_xarray(data: SITSData) -> xr.Dataset: - """Convert sits to xarray. +def _band_config_keys(cube: SITSCubeModel, band: str) -> tuple[str, ...]: + """Define the configuration keys of a cube band. Args: - data (pysits.models.SITSData): SITS Data. + cube (SITSCubeModel): Data cube. + + band (str): Band name. Returns: - xr.Dataset: SITS data as xarray.Dataset. + tuple[str, ...]: Configuration keys of the band. """ - # Metadata columns - time_series_metadata = data.drop(columns="time_series") + classes = list(cube._instance.rclass) + + # Cubes derived from a classification have their + # own configuration + if "derived_cube" in classes: + return ("derived_cube", classes[0], "bands", band) + + return ( + "sources", + str(cube["source"].iloc[0]), + "collections", + str(cube["collection"].iloc[0]), + "bands", + band, + ) - # Extract time-series column - time_series_data = data["time_series"] - # Convert to a list of data frames - time_series_data = time_series_data.tolist() +def _scaling_required(configs: dict[str, dict]) -> bool: + """Check if bands require scaling. - # Get time-series attributes (removing ``Index``) - time_series_attributes = set(time_series_data[0].columns).difference(["Index"]) - time_series_attributes = list(time_series_attributes) + Args: + configs (dict[str, dict]): Band configurations, indexed by band. - # Extract samples timeline - timeline = time_series_data[0]["Index"] + Returns: + bool: ``True`` when at least one band has a scale factor or an offset. + """ + for config in configs.values(): + # Get scale factor + scale_factor = config["scale_factor"] + + # Get offset value + offset_value = config["offset_value"] + + # Check which is correct and valid + is_scale_factor_valid = scale_factor is not None and scale_factor != 1 + is_offset_valid = offset_value is not None and offset_value != 0 + + # If at least one is correct + if is_scale_factor_valid or is_offset_valid: + # Scalling / offset is required! + return True + + # Fallback: Scaling is not applied + return False + + +def _band_property(configs: dict[str, dict], bands: list[str], prop: str, default): + """Extract a band property as an array, aligned with ``bands``.""" + return xr.DataArray( + data=[ + default if configs[band][prop] is None else configs[band][prop] + for band in bands + ], + dims="band", + coords={"band": bands}, + ) - # Drop ``Index`` and create a stack - time_series_data = np.stack( - [ts.drop(columns="Index").to_numpy() for ts in time_series_data] + +def band_config(cube: SITSCubeModel, band: str) -> dict: + """Get the configuration of a cube band. + + Args: + cube (SITSCubeModel): Data cube. + + band (str): Band name. + + Returns: + dict: Band ``scale_factor``, ``offset_value``, ``missing_value`` and + ``data_type``. + """ + keys = _band_config_keys(cube, band) + + return { + prop: sits_config_value(*keys, prop, default=None) + for prop in ("scale_factor", "offset_value", "missing_value", "data_type") + } + + +def apply_band_scaling( + data: xr.DataArray, configs: dict[str, dict], scale: bool +) -> xr.DataArray: + """Apply the sits configuration to cube values. + + Missing values are replaced by ``NaN`` and values are converted to their + physical range using the band scale factor and offset. + + Args: + data (xr.DataArray): Cube data, with a ``band`` dimension. + + configs (dict[str, dict]): Band configurations, indexed by band. + + scale (bool): Flag to scale values. + + Returns: + xr.DataArray: Data with the band configuration applied. + """ + # Get bands available + bands = [str(band) for band in data["band"].values] + + # Bands without scaling keep their values and data type (e.g., class cubes) + if scale and _scaling_required(configs): + missing_values = _band_property( + configs=configs, + bands=bands, + prop="missing_value", + default=np.nan, + ) + scale_factors = _band_property( + configs=configs, + bands=bands, + prop="scale_factor", + default=1, + ) + offset_values = _band_property( + configs=configs, + bands=bands, + prop="offset_value", + default=0, + ) + + # Scale only valid values + data = data.where(data != missing_values) + + # Scale! + data = (data * scale_factors + offset_values).astype("float32") + + # Keep the band configuration available to users + return data.assign_coords( + { + "scale_factor": _band_property( + configs=configs, + bands=bands, + prop="scale_factor", + default=np.nan, + ), + "offset_value": _band_property( + configs=configs, + bands=bands, + prop="offset_value", + default=np.nan, + ), + "missing_value": _band_property( + configs=configs, + bands=bands, + prop="missing_value", + default=np.nan, + ), + } ) - # Create xarray dataset - return xr.Dataset( - data_vars={ - var: (["sample", "time"], time_series_data[:, :, i]) - for i, var in enumerate(time_series_attributes) - }, - coords={ - "sample": np.arange(len(time_series_metadata)), - "time": timeline, - "longitude": ("sample", time_series_metadata["longitude"].to_numpy()), - "latitude": ("sample", time_series_metadata["latitude"].to_numpy()), - "label": ("sample", time_series_metadata["label"].to_numpy()), - "cube": ("sample", time_series_metadata["cube"].to_numpy()), - }, + +# +# Data cube conversions +# +def _load_grid_group( + cube: SITSCubeModel, + files: PandasDataFrame, + scale: bool, + chunks: dict, + cube_args: dict, +) -> xr.DataArray: + """Load all files of a grid group. + + Args: + cube (SITSCubeModel): Data cube. + + files (PandasDataFrame): Files of a single grid group. + + scale (bool): Flag to scale values. + + chunks (dict): Chunks used to read data. + + cube_args (dict): Extra arguments used to read data. + + Returns: + xr.DataArray: Group data, with dimensions ``(band, time, y, x)``. + """ + bands = sorted(files["band"].unique()) + configs = {band: band_config(cube, band) for band in bands} + + load_args = { + "chunks": chunks, + "groupby": "time", + "fail_on_error": True, + "geobox": files_geobox(files), + "patch_url": signature_file_url, + } + + # Data types and missing values are only used when shared by all bands + data_types = {configs[band]["data_type"] for band in bands} + missing_values = {configs[band]["missing_value"] for band in bands} + + # If there is one data type + if len(data_types) == 1 and None not in data_types: + # Get dtype + load_args["dtype"] = DATA_TYPES[data_types.pop()] + + # Only one missing value + if len(missing_values) == 1 and None not in missing_values: + # nodata value + load_args["nodata"] = missing_values.pop() + + # Update args if required + load_args.update(cube_args) + + # Read files + data = stac_items_from_files(files) + + # Transform int dataset + data = odc_load( + items=data, + bands=bands, + **load_args, ) + # Transform into dataarray + data = data.to_dataarray(dim="band") + + # Apply band scalling + return apply_band_scaling(data, configs, scale) + -def pandas_cube_as_xarray(cube: SITSData) -> xr.Dataset: - """Convert cube to xarray. +def raster_cube_as_xarray( + cube: SITSCubeModel, + bands: list[str] | None = None, + scale: bool = True, + chunks: dict | None = None, + cube_args: dict | None = None, +) -> xr.DataArray | xr.DataTree: + """Convert a data cube to xarray. Args: - cube (pysits.models.SITSData): Cube data + cube (SITSCubeModel): Data cube. + + bands (list[str]): Bands to use. When ``None``, all bands are used. + + scale (bool): Flag to scale values. + + chunks (dict): Chunks used to read data. + + cube_args (dict): Extra arguments used to read data. + + Returns: + xr.DataArray | xr.DataTree: Cube data with dimensions ``(band, time, y, x)``. + """ + arrays = {} + groups = cube_grid_groups(cube, bands) + + for index, files in enumerate(groups.values()): + arrays[grid_group_name(files, index)] = _load_grid_group( + cube=cube, + files=files, + scale=scale, + chunks=chunks or DEFAULT_CHUNKS, + cube_args=cube_args or {}, + ) + + # Cubes in a single grid are represented as arrays + if len(arrays) == 1: + return next(iter(arrays.values())) + + return xr.DataTree.from_dict( + {name: array.to_dataset(name=TREE_VARIABLE) for name, array in arrays.items()} + ) + + +# +# Derived data cube conversions +# +def _open_derived_file(path: str, chunks: dict | None) -> xr.DataArray: + """Open a file of a derived cube. + + Args: + path (str): File path. + + chunks (dict): Chunks used to read data. + + Returns: + xr.DataArray: File data, with dimensions ``(band, y, x)``. + """ + return xrio.open_rasterio(signature_file_url(path), chunks=chunks, masked=False) + + +def _derived_labels(cube: SITSCubeModel) -> list[str]: + """Get the labels of a derived cube.""" + labels = sits_labels(cube) + + return [str(label) for label in labels] + + +def _apply_derived_scaling( + data: xr.DataArray, config: dict, scale: bool +) -> xr.DataArray: + """Apply the sits configuration to derived cube values. + + Args: + data (xr.DataArray): Derived cube data. + + config (dict): Band configuration. + + scale (bool): Flag to scale values. Returns: - xr.Dataset: Cube data as xarray.Dataset. + xr.DataArray: Data with the band configuration applied. """ - # Get all files from the cube - cube_file_info = cube["file_info"].tolist() + # Files define their own missing value, which is used when available + missing_value = data.rio.nodata + + if missing_value is None: + missing_value = config["missing_value"] - # Merge and sort values - cube_file_info = pandas_concat(cube_file_info, ignore_index=True) - cube_file_info = cube_file_info.sort_values(["date", "band"]).reset_index(drop=True) + # Get scale and offset values + scale_factor = config["scale_factor"] + offset_value = config["offset_value"] - # Assuming all cube have the same CRS / resolution, use one file - # to extract ``shape``, ``coords`` and ``crs`` - cube_sample = cube_file_info.iloc[0]["path"] + # Test values + has_scale = scale_factor is not None and scale_factor != 1 + has_offset = offset_value is not None and offset_value != 0 - # Open file - cube_sample = xrio.open_rasterio(cube_sample, masked=True) + # Has any scaling ? + has_scaling = has_scale or has_offset - # Extract info - cube_crs = cube_sample.rio.crs - cube_res_x, cube_res_y = list(map(lambda x: abs(x), cube_sample.rio.resolution())) + # If there are scaling: keep training + if scale and has_scaling: + if missing_value is not None: + data = data.where(data != missing_value) - # To handle multiple tiles, use a ``global`` extent, covering all tiles - xmin = cube_file_info["xmin"].min() - xmax = cube_file_info["xmax"].max() - ymin = cube_file_info["ymin"].min() - ymax = cube_file_info["ymax"].max() + # Apply scale + offset + data = data * (scale_factor or 1) + (offset_value or 0) - # Calculate global shape - width = int(np.ceil((xmax - xmin) / cube_res_x)) - height = int(np.ceil((ymax - ymin) / cube_res_y)) + # Data as float32 + data = data.astype("float32") - # Define global transform - global_transform = Affine.translation(xmin, ymax) * Affine.scale( - cube_res_x, -cube_res_y + data.attrs.update( + { + "scale_factor": scale_factor, + "offset_value": offset_value, + "missing_value": missing_value, + } ) - # Define X and Y coordinates - x_coords = np.arange(width) * cube_res_x + xmin + cube_res_x / 2 - y_coords = ymax - np.arange(height) * cube_res_y - cube_res_y / 2 + return data + + +def derived_cube_as_xarray( + cube: SITSCubeModel, + scale: bool = True, + chunks: dict | None = None, +) -> xr.DataArray | xr.DataTree: + """Convert a cube derived from a classification to xarray. + + Args: + cube (SITSCubeModel): Derived data cube (e.g., class, probs). + + scale (bool): Flag to scale values. + + chunks (dict): Chunks used to read data. + + Returns: + xr.DataArray | xr.DataTree: Cube data. Probability cubes have + dimensions ``(label, y, x)``, class cubes ``(y, x)``, and the + remaining derived cubes ``(band, y, x)``. When the cube has files + in more than one grid, a data tree with one node per grid is + returned. + + Raises: + ValueError: When cube labels don't describe the data available in the + files of the cube. + """ + arrays = {} + classes = list(cube._instance.rclass) + + is_probs = "probs_cube" in classes + is_class = "class_cube" in classes - # Prepare variables by band and date - dataset_vars = {} + groups = cube_grid_groups(cube) - for band, band_data in cube_file_info.groupby("band"): - data_arrays = [] - time_coords = [] + # For groups defined + for index, files in enumerate(groups.values()): + tiles = [] - # Sort by date - band_data_sorted = band_data.sort_values("date") + # Iterate files + for _, file in files.iterrows(): + band = str(file["band"]) + config = band_config(cube=cube, band=band) - # Iterate bands - for _, row in band_data_sorted.iterrows(): - # Get info - da_path = row["path"] - da_date = row["date"] + # Open derived file + data = _open_derived_file( + path=str(file["path"]), + chunks=chunks or DEFAULT_CHUNKS, + ) - # Load raster (band / date) - da_raster = _xarray_load_raster( - raster_path=da_path, - crs=cube_crs, - shape=(height, width), - transform=global_transform, + # Case: probs cube + if is_probs: + # Get labels + labels = _derived_labels(cube) + + # If there are more labels than bands, assume there is + # something wrong + if data.sizes["band"] != len(labels): + raise ValueError( + f"Cube has {len(labels)} labels, but its files have " + f"{data.sizes['band']} layers. Check the labels used to " + "create the cube." + ) + + # Rename label with band + data = data.rename({"band": "label"}) + + # Add coordinates + data = data.assign_coords(label=labels) + + # Case: class cube + elif is_class: + data = data.squeeze("band", drop=True) + + # Case: Any other case + else: + data = data.assign_coords(band=[band]) + + # Apply scaling + data = _apply_derived_scaling( + data=data, + config=config, + scale=scale, ) - # Save data - data_arrays.append(da_raster) - time_coords.append(da_date) + # Save result + tiles.append(data) - # Stack data - stacked = dask_array.stack(data_arrays, axis=0) - - # Save as variable (all bands in ``time``, ``y`` and ``x``) - dataset_vars[band] = (("time", "y", "x"), stacked) - - # Build data cube - ds = xr.Dataset( - data_vars=dataset_vars, - coords={ - "time": pandas_to_datetime(time_coords), - "y": y_coords, - "x": x_coords, - }, + # Generate one main array + array = tiles[0] if len(tiles) == 1 else xr.combine_by_coords(tiles) + + # Keep cube metadata available to users + array.attrs.update( + { + "start_date": str(files["start_date"].min()), + "end_date": str(files["end_date"].max()), + } + ) + + # If class, save labels as metadata as well + if is_class: + array.attrs["labels"] = dict(enumerate(_derived_labels(cube), start=1)) + + # Save grid + arrays[grid_group_name(files, index)] = array + + # Just return it + if len(arrays) == 1: + return next(iter(arrays.values())) + + # If there are more than one array, return a data tree + return xr.DataTree.from_dict( + {name: array.to_dataset(name=TREE_VARIABLE) for name, array in arrays.items()} ) - # Save CRS - ds.rio.write_crs(cube_crs, inplace=True) - return ds +# +# Time series conversions +# +def time_series_as_xarray(data: SITSData, bands: list[str] | None = None) -> xr.Dataset: + """Convert time series to xarray. + + Args: + data (SITSData): Time series data. + + bands (list[str]): Bands to use. When ``None``, all bands are used. + + Returns: + xr.Dataset: Time series with dimensions ``(sample, time)``. Samples + with different timelines are aligned, using ``NaN`` in the dates + they don't have. + + Raises: + ValueError: When a band is not available in the time series. + """ + metadata = data.drop(columns="time_series") + time_series = data["time_series"].tolist() + + # Bands available in the time series + available_bands = [column for column in time_series[0].columns if column != "Index"] + + if bands is not None: + missing_bands = [band for band in bands if band not in available_bands] + + if missing_bands: + raise ValueError( + f"Bands not available in the time series: {', '.join(missing_bands)}. " + f"Available bands: {', '.join(available_bands)}" + ) + + available_bands = list(bands) + + # Samples can have different timelines, which are aligned when required + timelines = [ts["Index"] for ts in time_series] + + # Define the shared timeline dates + shared_timeline = all( + len(timeline) == len(timelines[0]) and timeline.equals(timelines[0]) + for timeline in timelines + ) + + # If there are shared dates, stack it + if shared_timeline: + # Stack data + values = [ts[available_bands].to_numpy() for ts in time_series] + values = np.stack(values) + + # Data as sample + samples = xr.DataArray( + values, + dims=("sample", "time", "band"), + coords={ + "sample": np.arange(len(time_series)), + "time": timelines[0], + "band": available_bands, + }, + ) + + # Otherwise, concat it + else: + samples = xr.concat( + [ + xr.DataArray( + data=ts[available_bands].to_numpy(), + dims=("time", "band"), + coords={"time": ts["Index"], "band": available_bands}, + ) + for ts in time_series + ], + dim="sample", + join="outer", + ) + + # Assign coordinates + samples = samples.assign_coords(sample=np.arange(len(time_series))) + + # Keep sample metadata available to users + properties = ("longitude", "latitude", "label", "cube", "start_date", "end_date") + + coords = { + column: ("sample", metadata[column].to_numpy()) + for column in properties + if column in metadata.columns + } + + # Return! + return samples.assign_coords(coords).to_dataset(dim="band") diff --git a/pysits/models/data/cube.py b/pysits/models/data/cube.py index ba32e13..4c44595 100644 --- a/pysits/models/data/cube.py +++ b/pysits/models/data/cube.py @@ -22,9 +22,9 @@ from rpy2.robjects.vectors import DataFrame as RDataFrame from pysits.backend.functions import r_fnc_set_column -from pysits.conversions.tibble_arrow import ( - pandas_cube_to_tibble_arrow, - tibble_cube_to_pandas_arrow, +from pysits.conversions.tibble import ( + pandas_cube_to_tibble, + tibble_cube_to_pandas, ) from pysits.models.data.frame import SITSFrame @@ -88,7 +88,7 @@ def __init__(self, data: PandasSeries, **kwargs) -> None: cube_data = PandasDataFrame([data]) # Convert to R DataFrame - self._instance = pandas_cube_to_tibble_arrow(cube_data) + self._instance = pandas_cube_to_tibble(cube_data) # Initialize super class super().__init__(data=data, **kwargs) @@ -139,8 +139,12 @@ def __init__(self, instance, **kwargs): col in instance.columns for col in self.required_columns ) - if has_required_columns: - self._instance = pandas_cube_to_tibble_arrow(instance) + # Selections can have no rows, which R can't represent as a cube + if has_required_columns and not instance.empty: + self._instance = pandas_cube_to_tibble(instance) + + else: + self._instance = None else: self._instance = instance @@ -161,7 +165,7 @@ def _convert_from_r(self, instance: RDataFrame) -> PandasDataFrame: Args: instance (rpy2.robjects.vectors.DataFrame): Data instance. """ - return tibble_cube_to_pandas_arrow(instance) + return tibble_cube_to_pandas(instance) # # Data management @@ -171,6 +175,10 @@ def _sync_instance(self): if not self._is_updated: return + # Cubes with no rows are not available in R + if self._instance is None or self.empty: + return + # Save current classes classes = self._instance.rclass @@ -179,12 +187,12 @@ def _sync_instance(self): if "base_info" in self.columns: # Convert each dataframe in the series to R DataFrame - base_info = [pandas_cube_to_tibble_arrow(df) for df in self.base_info] + base_info = [pandas_cube_to_tibble(df) for df in self.base_info] # Drop base_info self.drop(columns=["base_info"], inplace=True) - self._instance = pandas_cube_to_tibble_arrow(self) + self._instance = pandas_cube_to_tibble(self) # Add base_info if base_info is not None: diff --git a/pysits/models/data/ts.py b/pysits/models/data/ts.py index 651138e..ce188f2 100644 --- a/pysits/models/data/ts.py +++ b/pysits/models/data/ts.py @@ -22,10 +22,9 @@ from pandas import Series as PandasSeries from rpy2.robjects.vectors import DataFrame as RDataFrame -from pysits.conversions.tibble import tibble_sits_to_pandas -from pysits.conversions.tibble_arrow import ( - pandas_sits_to_tibble_arrow, - tibble_sits_to_pandas_arrow, +from pysits.conversions.tibble import ( + pandas_sits_to_tibble, + tibble_sits_to_pandas, ) from pysits.models.data.frame import SITSFrame, SITSFrameSF @@ -63,7 +62,7 @@ def __init__(self, data: PandasSeries, **kwargs): ts_data = PandasDataFrame([data]) # Convert to R DataFrame - self._instance = pandas_sits_to_tibble_arrow(ts_data) + self._instance = pandas_sits_to_tibble(ts_data) # Initialize super class super().__init__(data=data, **kwargs) @@ -77,7 +76,7 @@ def __init__(self, instance, **kwargs): # If instance is a Pandas DataFrame, convert to R cube if isinstance(instance, PandasDataFrame): # Convert to R DataFrame - self._instance = pandas_sits_to_tibble_arrow(instance) + self._instance = pandas_sits_to_tibble(instance) else: self._instance = instance @@ -106,7 +105,7 @@ def _convert_from_r(self, instance: RDataFrame, **kwargs) -> PandasDataFrame: Args: instance (rpy2.robjects.vectors.DataFrame): Data instance. """ - return tibble_sits_to_pandas_arrow(instance) + return tibble_sits_to_pandas(instance) # # Data management @@ -120,7 +119,7 @@ def _sync_instance(self): classes = self._instance.rclass # Update instance - self._instance = pandas_sits_to_tibble_arrow(self) + self._instance = pandas_sits_to_tibble(self) # Restore classes self._instance.rclass = classes diff --git a/pysits/models/data/tuning.py b/pysits/models/data/tuning.py index 6963701..28ec042 100644 --- a/pysits/models/data/tuning.py +++ b/pysits/models/data/tuning.py @@ -18,13 +18,12 @@ """Tuning data models.""" from collections.abc import Callable -from itertools import chain from typing import Any from rpy2.rinterface_lib.sexp import NULLType from rpy2.robjects.vectors import ListVector -from pysits.conversions.common import convert_to_python +from pysits.conversions.common import convert_to_python, eval_r_language from pysits.models.data.base import SITSData from pysits.models.data.matrix import SITSConfusionMatrix from pysits.models.data.ts import SITSTimeSeriesModel @@ -217,11 +216,7 @@ def optimizer(self) -> list[str]: Returns: A list of strings representing the optimizer configurations. """ - return list( - chain.from_iterable( - [[x.rclass[0] for x in self._instance.rx2("optimizer")]] - ) - ) + return [eval_r_language(x).rclass[0] for x in self._instance.rx2("optimizer")] @property def opt_hparams(self) -> list[dict[str, float]]: diff --git a/pysits/models/frame.py b/pysits/models/frame.py index 8fc35f8..7e517f7 100644 --- a/pysits/models/frame.py +++ b/pysits/models/frame.py @@ -17,175 +17,21 @@ """Pandas extension models.""" -import warnings -from collections.abc import Sequence - -import numpy as np from pandas import DataFrame as PandasDataFrame -from pandas._typing import Self -from pandas.api.extensions import ( - ExtensionArray, - ExtensionDtype, - register_extension_dtype, -) - - -@register_extension_dtype -class SITSFrameDtype(ExtensionDtype): - """SITS Frame dtype. - - Note: - To learn more about the operations and attributes implemented on this class - it is recommended to check the pandas documentation: - https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.api.extensions.ExtensionDtype.html - """ - - name = "sits" - """A string identifying the data type.""" - - type = PandasDataFrame - """The scalar type for the array.""" - - kind = "O" - """A character code (one of `biufcmMOSUV`) identifying the general kind of data. - - To learn more, please check: https://numpy.org/doc/stable/reference/generated/numpy.dtype.kind.html - """ - - isnative = True - """Whether the dtype is native.""" - - # - # Class methods - # - @classmethod - def construct_array_type(cls): - return SITSFrameArray -class SITSFrameArray(ExtensionArray): - """SITS Frame array type. +class NestedFrame(PandasDataFrame): + """A lightweight DataFrame subclass for nested column cells. - Note: - To learn more about the operations and attributes implemented on this class - it is recommended to check the pandas documentation: - https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.api.extensions.ExtensionDtype.html + All pandas operations work natively. Overrides ``__repr__`` to show + a compact summary instead of the full table. """ - def __init__(self, frames): - """Initializer.""" - self._data = frames - - # - # Properties - # @property - def dtype(self): - """Dtype value.""" - return SITSFrameDtype() - - # - # Private methods - # - def _formatter(self, boxed=False): - """Formatting function for scalar values.""" - return lambda x: f"NestedDataFrame(size = {len(x)})" - - # - # Class methods - # - @classmethod - def _from_sequence(cls, scalars, dtype=None, copy=False): - """Construct a new ExtensionArray from a sequence of scalars.""" - return cls(scalars) - - @classmethod - def _concat_same_type(cls, to_concat: Sequence[Self]) -> Self: - """Concatenate multiple array of this dtype. - - Args: - to_concat (Sequence[Self]): The sequence of arrays to concatenate. - - Returns: - Self: The concatenated array. - - Examples: - >>> arr1 = SITSFrameArray([df1, df2]) - >>> arr2 = SITSFrameArray([df3, df4]) - >>> SITSFrameArray._concat_same_type([arr1, arr2]) - NestedDataFrame(size = 4) - """ - concatenated_data = [df for arr in to_concat for df in arr._data] - return cls(concatenated_data) - - # - # Dunder methods (magic methods) - # - def __getitem__(self, item): - """Get item.""" - # Scalar `item` index - if isinstance(item, int): - return self._data[item] - - # Assuming `item` as a sequence - frame_size = len(self._data) - - if isinstance(item, slice): - # Safely handle ``slice`` start/stop/step - indices = range(*item.indices(frame_size)) - - else: - # Assume it's iterable (like list or set) - indices = item - - # Transform and return! - return SITSFrameArray([self._data[idx] for idx in indices]) - - def __len__(self): - """Get object size.""" - return len(self._data) + def _constructor(self): + return NestedFrame def __repr__(self): - """Object representation.""" - return f"NestedDataFrame(size = {len(self._data)})" - - def __eq__(self, other): - """Compare two SITSFrameArray objects.""" - if isinstance(other, SITSFrameArray): - return np.array( - [ - x.equals(y) if hasattr(x, "equals") else np.array_equal(x, y) - for x, y in zip(self._data, other._data) - ], - dtype=bool, - ) - - return NotImplemented - - # - # Hashing - # - def __hash__(self) -> int: - """Hash the array.""" - return hash(self._data) - - # - # Operations - # - def take(self, indices, allow_fill=False, fill_value=None): - """Take elements from an array.""" - with warnings.catch_warnings(): - # Ignoring pandas warning about non-standard input. This - # will be solved soon - warnings.simplefilter("ignore") - - # Take and return! - return SITSFrameArray([self._data[idx] for idx in indices]) - - def isna(self): - """A 1-D array indicating if each value is missing.""" - return np.array([df is None for df in self._data]) - - def copy(self): - """Return a copy of the array.""" - return SITSFrameArray(self._data.copy()) + """Compact representation.""" + nrows, ncols = self.shape + return f"NestedFrame({nrows} x {ncols})" diff --git a/pysits/models/ml.py b/pysits/models/ml.py index 64442fa..e0802f0 100644 --- a/pysits/models/ml.py +++ b/pysits/models/ml.py @@ -25,3 +25,7 @@ # class SITSMachineLearningMethod(SITSBase): """ML Method base class.""" + + +class SITSRepresentationLearningMethod(SITSBase): + """RL Method base class.""" diff --git a/pysits/settings.py b/pysits/settings.py index 45f0b52..69a5f3c 100644 --- a/pysits/settings.py +++ b/pysits/settings.py @@ -39,11 +39,11 @@ # # Compatible sits version # -__sitsver__ = "1.5.4" +__sitsver__ = "2.0.0" # # Package version # -__version__ = "1.5.4" +__version__ = "2.0.0" __all__ = ("__version__",) diff --git a/pysits/sits/config.py b/pysits/sits/config.py index 371241b..e3bc41f 100644 --- a/pysits/sits/config.py +++ b/pysits/sits/config.py @@ -17,12 +17,57 @@ """Configuration operations.""" +from typing import Any + +from pysits.backend.functions import r_fnc_sits_conf from pysits.backend.pkgs import r_pkg_sits from pysits.conversions.decorators import function_call from pysits.docs import attach_doc from pysits.models.data.base import SITStructureData +def sits_config_value(*keys: str, default: Any = ...) -> Any: + """Get a value from the sits configuration. + + Configuration values are addressed by the sequence of keys leading to them. + As an example, the scale factor of a collection band is available at + ``("sources", , "collections", , "bands", , + "scale_factor")``. + + Args: + *keys (str): Sequence of configuration keys. + + default (Any): Value returned when the keys are not available. When it + is not defined, missing keys raise a ``KeyError``. + + Returns: + Any: Configuration value. Values with a single element are returned as + scalars. + + Raises: + KeyError: When the keys are not available and no ``default`` is defined. + + Examples: + >>> sits_config_value( + ... "sources", "BDC", "collections", "MOD13Q1-6.1", + ... "bands", "NDVI", "scale_factor" + ... ) + 0.0001 + """ + try: + value = list(r_fnc_sits_conf(*keys)) + + except Exception as e: + if default is ...: + raise KeyError( + f"There is no sits configuration value for: {' -> '.join(keys)}" + ) from e + + return default + + return value[0] if len(value) == 1 else value + + @function_call(r_pkg_sits.sits_config, SITStructureData) @attach_doc("sits_config") def sits_config(*args, **kwargs) -> SITStructureData: @@ -43,3 +88,9 @@ def sits_config_show(*args, **kwargs) -> None: @attach_doc("sits_config_user_file") def sits_config_user_file(*args, **kwargs) -> None: """Create a user configuration file.""" + + +@function_call(r_pkg_sits.sits_parallel, lambda x: None) +@attach_doc("sits_parallel") +def sits_parallel(*args, **kwargs) -> None: + """Create sits cluster.""" diff --git a/pysits/sits/exporters/xarray.py b/pysits/sits/exporters/xarray.py index 0eca611..f7cdecb 100644 --- a/pysits/sits/exporters/xarray.py +++ b/pysits/sits/exporters/xarray.py @@ -19,13 +19,26 @@ from functools import singledispatch +from pysits.docs import attach_doc from pysits.models.data.cube import SITSCubeModel from pysits.models.data.frame import SITSFrame from pysits.models.data.ts import SITSTimeSeriesModel +# +# Vector cubes +# +VECTOR_CUBE_CLASSES = ( + "vector_cube", + "segs_cube", + "probs_vector_cube", + "class_vector_cube", + "uncertainty_vector_cube", + "variance_vector_cube", +) + @singledispatch -def _sits_as_xarray(data: SITSFrame): +def _sits_as_xarray(data: SITSFrame, **kwargs): """sits as xarray dispatch.""" raise NotImplementedError( f"There is no `sits_as_xarray` available for {type(data)}" @@ -33,33 +46,86 @@ def _sits_as_xarray(data: SITSFrame): @_sits_as_xarray.register -def _(data: SITSTimeSeriesModel): - """Convert sits to xarray.""" - from pysits.conversions.xarray import pandas_sits_as_xarray +def _(data: SITSTimeSeriesModel, bands=None, scale=True, chunks=None, cube_args=None): + """Convert time series to xarray.""" + from pysits.conversions.xarray import time_series_as_xarray + + # Time series are available in memory, so they are not read in chunks + if chunks is not None or cube_args is not None: + raise ValueError("`chunks` and `cube_args` are only available for data cubes.") - return pandas_sits_as_xarray(data) + # Load time-series + return time_series_as_xarray(data, bands=bands) @_sits_as_xarray.register -def _(data: SITSCubeModel): - """Convert cube to xarray.""" - from pysits.conversions.xarray import pandas_cube_as_xarray +def _(data: SITSCubeModel, bands=None, scale=True, chunks=None, cube_args=None): + """Convert a data cube to xarray.""" + from pysits.conversions.xarray import ( + derived_cube_as_xarray, + raster_cube_as_xarray, + ) - return pandas_cube_as_xarray(data) + # Get cube classes + cube_classes = list(data._instance.rclass) + # Define if it is a vector cube + is_vector_cube = [cls for cls in cube_classes if cls in VECTOR_CUBE_CLASSES] -def sits_as_xarray(data: SITSFrame): - """Convert data to xarray.""" - try: - from pysits.conversions.xarray import ( - pandas_cube_as_xarray, # noqa - pandas_sits_as_xarray, # noqa + # If it is a vector cube, raise an error + if is_vector_cube: + raise NotImplementedError( + f"There is no `sits_as_xarray` available for `{is_vector_cube[0]}`. " + "Use `sits_as_geopandas` to export cubes with segments." ) + # Cubes derived from a classification have no time dimension + if "derived_cube" in cube_classes: + if bands is not None: + raise ValueError( + "`bands` is not available for cubes derived from a classification." + ) + + # Load derived cube + return derived_cube_as_xarray( + cube=data, + scale=scale, + chunks=chunks, + ) + + # Load raster cube + return raster_cube_as_xarray( + cube=data, + bands=bands, + scale=scale, + chunks=chunks, + cube_args=cube_args, + ) + + +@attach_doc("sits_as_xarray") +def sits_as_xarray( + data: SITSFrame, + *, + bands: list[str] | None = None, + scale: bool = True, + chunks: dict | None = None, + cube_args: dict | None = None, +): + """Export a sits data object as an xarray object.""" + try: + import pysits.conversions.xarray # noqa: F401 + except ImportError as e: raise ImportError( "xarray dependencies not installed. To use this feature, please install " "them with `pip install pysits[xarray]`." ) from e - return _sits_as_xarray(data) + return _sits_as_xarray( + data, + bands=bands, + scale=scale, + chunks=chunks, + cube_args=cube_args, + ) diff --git a/pysits/sits/impute.py b/pysits/sits/impute.py index 3b6ec45..42b9d70 100644 --- a/pysits/sits/impute.py +++ b/pysits/sits/impute.py @@ -17,11 +17,7 @@ """Impute operations.""" -from pysits.backend.pkgs import r_pkg_sits from pysits.conversions.clojure import closure_factory -from pysits.conversions.decorators import function_call -from pysits.docs import attach_doc -from pysits.models.data.ts import SITSTimeSeriesModel # # Impute methods @@ -30,12 +26,3 @@ impute_mean = closure_factory("impute_mean") impute_median = closure_factory("impute_median") impute_mean_window = closure_factory("impute_mean_window") - - -# -# High-level utility operations -# -@function_call(r_pkg_sits.sits_impute, SITSTimeSeriesModel) -@attach_doc("sits_impute") -def sits_impute(*args, **kwargs) -> SITSTimeSeriesModel: - """Impute missing values in a time series.""" diff --git a/pysits/sits/ml.py b/pysits/sits/ml.py index 2fcf242..1bb2f3b 100644 --- a/pysits/sits/ml.py +++ b/pysits/sits/ml.py @@ -25,7 +25,7 @@ from pysits.conversions.common import convert_dict_like_as_list_to_r from pysits.conversions.decorators import function_call from pysits.docs import attach_doc -from pysits.models.ml import SITSMachineLearningMethod +from pysits.models.ml import SITSMachineLearningMethod, SITSRepresentationLearningMethod from pysits.models.resolver import resolve_and_invoke_accuracy_class @@ -76,6 +76,17 @@ def convert_opt_hparams(obj: Any) -> Any: sits_lightgbm = closure_factory("sits_lightgbm") +# +# Encoder Methods +# +sits_ssl_mae = closure_factory("sits_ssl_mae") +sits_ssl_lejepa = closure_factory("sits_ssl_lejepa") +sits_ssl_vicreg = closure_factory("sits_ssl_vicreg") + +sits_barlow_twins = closure_factory("sits_barlow_twins") +sits_contrastive_learning = closure_factory("sits_contrastive_learning") + + # # Extra parameters - SVM # @@ -92,6 +103,12 @@ def sits_train(*args, **kwargs) -> SITSMachineLearningMethod: """Train a machine learning model.""" +@function_call(r_pkg_sits.sits_pre_train, SITSRepresentationLearningMethod) +@attach_doc("sits_pre_train") +def sits_pre_train(*args, **kwargs) -> SITSRepresentationLearningMethod: + """Pre-train representation learning models for sits.""" + + @function_call(r_pkg_sits.sits_kfold_validate, resolve_and_invoke_accuracy_class) @attach_doc("sits_kfold_validate") def sits_kfold_validate(*args, **kwargs) -> resolve_and_invoke_accuracy_class: diff --git a/pysits/sits/tiles.py b/pysits/sits/tiles.py index 9e789e5..ec64886 100644 --- a/pysits/sits/tiles.py +++ b/pysits/sits/tiles.py @@ -24,12 +24,6 @@ from pysits.models.data.vector import SITSNamedVector -@function_call(r_pkg_sits.sits_mgrs_to_roi, SITSNamedVector) -@attach_doc("sits_mgrs_to_roi") -def sits_mgrs_to_roi(*args, **kwargs) -> SITSNamedVector: - """Convert MGRS to ROI.""" - - @function_call(r_pkg_sits.sits_tiles_to_roi, SITSNamedVector) @attach_doc("sits_tiles_to_roi") def sits_tiles_to_roi(*args, **kwargs) -> SITSNamedVector: diff --git a/pysits/sits/ts.py b/pysits/sits/ts.py index 4ac525e..660a4ab 100644 --- a/pysits/sits/ts.py +++ b/pysits/sits/ts.py @@ -32,6 +32,12 @@ # # High-level operation # +@function_call(r_pkg_sits.sits_encode, SITSTimeSeriesModel) +@attach_doc("sits_encode") +def sits_encode(*args, **kwargs) -> SITSTimeSeriesModel: + """Encode time series or data cubes.""" + + @function_call(r_pkg_sits.sits_get_data, SITSTimeSeriesModel) @attach_doc("sits_get_data") def sits_get_data(*args, **kwargs) -> SITSTimeSeriesModel: @@ -192,21 +198,6 @@ def sits_patterns(*args, **kwargs) -> SITSTimeSeriesPatternsModel: """Find temporal patterns associated to a set of time series.""" -# -# Filtering -# -@function_call(r_pkg_sits.sits_sgolay, SITSTimeSeriesModel) -@attach_doc("sits_sgolay") -def sits_sgolay(*args, **kwargs) -> SITSTimeSeriesModel: - """Apply Savitzky-Golay filter to time series.""" - - -@function_call(r_pkg_sits.sits_whittaker, SITSTimeSeriesModel) -@attach_doc("sits_whittaker") -def sits_whittaker(*args, **kwargs) -> SITSTimeSeriesModel: - """Apply Whittaker filter to time series.""" - - # # Distances # diff --git a/pysits/sits/visualization.py b/pysits/sits/visualization.py index 4ad4cf0..77f1c88 100644 --- a/pysits/sits/visualization.py +++ b/pysits/sits/visualization.py @@ -29,7 +29,7 @@ SITSTimeSeriesModel, SITSTimeSeriesPatternsModel, ) -from pysits.models.ml import SITSMachineLearningMethod +from pysits.models.ml import SITSMachineLearningMethod, SITSRepresentationLearningMethod from pysits.visualization import plot_base, plot_leaflet, plot_tmap @@ -120,3 +120,10 @@ def _(data: SITSTimeSeriesPatternsModel, **kwargs) -> None: def _(data: SITSMachineLearningMethod, **kwargs) -> None: """Plot machine learning method.""" return plot_base(data, **kwargs) + + +@sits_plot.register +@rpy2_fix_type +def _(data: SITSRepresentationLearningMethod, **kwargs) -> None: + """Plot representation learning method.""" + return plot_base(data, **kwargs) diff --git a/pysits/visualization/base.py b/pysits/visualization/base.py index 9f6fc5c..7025517 100644 --- a/pysits/visualization/base.py +++ b/pysits/visualization/base.py @@ -81,7 +81,13 @@ def _base_plot( # Handle plots if multiple: for i, plot in enumerate(plots): - for index, figure in enumerate(plot): + try: + figures = list(plot) + + except TypeError: + figures = [plot] + + for index, figure in enumerate(figures): file_path = os.path.join(temp_dir, f"base_plot_{i}_{index}.jpeg") # Enable image device diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..4bdb99c --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,81 @@ +# +# Copyright (C) 2025 sits developers. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, see . +# + +"""Unit tests for sits configuration.""" + +from pathlib import Path + +import pytest +from rpy2.rinterface_lib.embedded import RRuntimeError +from rpy2.rinterface_lib.sexp import NULLType + +from pysits.backend.loaders import load_function_from_package +from pysits.sits.config import sits_parallel + + +def _cluster(): + """Get the current sits cluster.""" + return load_function_from_package("sits::sits_parallel")() + + +@pytest.fixture(autouse=True) +def stop_cluster(): + """Fixture to guarantee no cluster is left open by the tests.""" + yield + + sits_parallel(0) + + +def test_sits_parallel_start(): + """Test sits parallel cluster creation.""" + assert sits_parallel(2) is None + assert not isinstance(_cluster(), NULLType) + + +def test_sits_parallel_restart(): + """Test sits parallel cluster restart.""" + sits_parallel(2) + + assert sits_parallel(3) is None + assert not isinstance(_cluster(), NULLType) + + +@pytest.mark.parametrize("workers", [0, 1]) +def test_sits_parallel_stop(workers: int): + """Test sits parallel cluster stop.""" + sits_parallel(2) + + assert sits_parallel(workers) is None + assert isinstance(_cluster(), NULLType) + + +def test_sits_parallel_with_log(tmp_path: Path): + """Test sits parallel cluster creation with log enabled.""" + assert sits_parallel(2, log=True, output_dir=tmp_path.as_posix()) is None + assert not isinstance(_cluster(), NULLType) + + +def test_sits_parallel_invalid_workers(): + """Test sits parallel with an invalid number of workers.""" + with pytest.raises(RRuntimeError): + sits_parallel(-1) + + +def test_sits_parallel_log_without_output_dir(): + """Test sits parallel with log enabled and no output directory.""" + with pytest.raises(RRuntimeError): + sits_parallel(2, log=True) diff --git a/tests/test_conversions.py b/tests/test_conversions.py index 7840d10..6d84808 100644 --- a/tests/test_conversions.py +++ b/tests/test_conversions.py @@ -19,13 +19,28 @@ import pytest import rpy2.robjects as ro +from geopandas import GeoDataFrame +from pandas import DataFrame as PandasDataFrame +from shapely.geometry import Point from pysits.conversions.clojure import closure_factory from pysits.conversions.common import ( convert_dict_like_as_list_to_r, convert_dict_like_to_r, convert_list_like_to_r, + convert_to_python, ) +from pysits.conversions.tibble import ( + _column_to_datetime, + geopandas_to_tibble, + pandas_cube_to_tibble, + pandas_sits_to_tibble, + pandas_to_tibble, + tibble_nested_to_pandas, +) +from pysits.conversions.vector import matrix_to_pandas, table_to_pandas +from pysits.models.frame import NestedFrame +from pysits.sits.context import samples_modis_ndvi def test_closure_factory_invalid_function(): @@ -163,3 +178,155 @@ def test_convert_dict_like_as_list_to_r(): # Empty dictionary empty_result = convert_dict_like_as_list_to_r({}) assert isinstance(empty_result, ro.vectors.ListVector) + + +def test_convert_to_python_r_language(): + """Test conversion of unevaluated R expressions.""" + # R expression (e.g., as returned by ``sits_tuning`` hyper-parameters) + result = convert_to_python(ro.r("quote(c(256, 256, 256))"), as_type="float") + + assert result == [256.0, 256.0, 256.0] + + # R expression nested in a list (e.g., ``opt_hparams``) + nested_result = convert_to_python( + ro.r("list(lr = 0.001, betas = quote(c(0.9, 0.999)))"), as_type="float" + ) + + assert nested_result == [{"lr": [0.001]}, {"betas": [0.9, 0.999]}] + + +def test_matrix_to_pandas(): + """Test conversion of a named R matrix.""" + matrix = ro.r('matrix(1:4, nrow = 2, dimnames = list(c("a", "b"), c("x", "y")))') + result = matrix_to_pandas(matrix) + + assert result.index.tolist() == ["a", "b"] + assert result.columns.tolist() == ["x", "y"] + assert result["x"].tolist() == [1, 2] + assert result["y"].tolist() == [3, 4] + + # Matrices holding vectors have their single-element cells unwrapped + list_matrix = ro.r( + 'matrix(list(1, 2, 3, 4), nrow = 2, dimnames = list(c("a", "b"), c("x", "y")))' + ) + + assert matrix_to_pandas(list_matrix)["x"].tolist() == [1.0, 2.0] + + +def test_table_to_pandas(): + """Test conversion of 1D and 2D R tables.""" + # 1D table + result_1d = table_to_pandas(ro.r('table(factor(c("a", "b", "a")))')) + + assert result_1d.index.tolist() == ["a", "b"] + assert result_1d.iloc[:, 0].tolist() == [2, 1] + + # 2D table + result_2d = table_to_pandas(ro.r('table(factor(c("a", "b")), factor(c("x", "x")))')) + + assert result_2d.index.tolist() == ["a", "b"] + assert result_2d.columns.tolist() == ["x"] + + # Tables with more than two dimensions are not supported + with pytest.raises(ValueError, match="Only 1D and 2D tables"): + table_to_pandas( + ro.r('table(factor("a"), factor("b"), factor("c"))'), + ) + + +def test_column_to_datetime(): + """Test conversion of R date offsets to datetime columns.""" + data = _column_to_datetime(PandasDataFrame({"start_date": [0, 366]}), "start_date") + + assert data["start_date"].dt.strftime("%Y-%m-%d").tolist() == [ + "1970-01-01", + "1971-01-02", + ] + + # Missing columns are ignored + assert "end_date" not in _column_to_datetime(data, "end_date").columns + + +def test_pandas_to_tibble(): + """Test conversion of a pandas DataFrame to an R data frame.""" + result = pandas_to_tibble(PandasDataFrame({"a": [1, 2], "b": ["x", "y"]})) + + assert isinstance(result, ro.vectors.DataFrame) + assert list(result.colnames) == ["a", "b"] + + +def test_tibble_nested_to_pandas(): + """Test conversion of an R tibble with nested data frames.""" + data = ro.r("tibble::tibble(id = 1L, ts = list(tibble::tibble(v = c(1, 2))))") + result = tibble_nested_to_pandas(data, nested_columns=["ts"]) + + assert result["id"].tolist() == [1] + assert isinstance(result["ts"][0], NestedFrame) + assert result["ts"][0]["v"].tolist() == [1.0, 2.0] + + # Nested frames are shown as a compact summary + assert repr(result["ts"][0]) == "NestedFrame(2 x 1)" + + +def test_geopandas_to_tibble(): + """Test conversion of a GeoDataFrame to an R sf object.""" + data = GeoDataFrame( + data={"a": [1]}, + geometry=[Point(0, 0)], + crs="EPSG:4326", + ) + result = geopandas_to_tibble(data) + + assert "sf" in list(ro.r["class"](result)) + assert "a" in list(ro.r["names"](result)) + + +def test_geopandas_to_tibble_without_crs(): + """Test conversion of a GeoDataFrame without CRS.""" + data = GeoDataFrame( + data={"a": [1]}, + geometry=[Point(0, 0)], + ) + + with pytest.raises(ValueError, match="must have a CRS"): + geopandas_to_tibble(data) + + +def test_geopandas_to_tibble_with_nested_columns(): + """Test conversion of a GeoDataFrame with embedded DataFrames.""" + data = GeoDataFrame( + data={"a": [1], "nested": [PandasDataFrame({"v": [1]})]}, + geometry=[Point(0, 0)], + crs="EPSG:4326", + ) + + with pytest.warns(UserWarning, match="Dropping columns with embedded DataFrames"): + result = geopandas_to_tibble(data) + + assert "nested" not in list(ro.r["names"](result)) + + +def test_pandas_sits_to_tibble(): + """Test conversion of a sits pandas DataFrame to an R tibble.""" + data = PandasDataFrame(samples_modis_ndvi.head(2)).copy() + result = pandas_sits_to_tibble(data) + + assert list(result.rclass) == ["sits", "tbl_df", "tbl", "data.frame"] + + # Optional columns define additional classes + data["predicted"] = data["time_series"] + data["base_data"] = data["time_series"] + data["id_sample"] = [1, 2] + data["id_neuron"] = [1, 2] + + rclass = list(pandas_sits_to_tibble(data).rclass) + + assert "predicted" in rclass + assert "sits_base" in rclass + assert "som_clean_samples" in rclass + + +def test_pandas_cube_to_tibble_empty(): + """Test conversion of a cube pandas DataFrame without tiles.""" + with pytest.raises(ValueError, match="at least one tile"): + pandas_cube_to_tibble(PandasDataFrame()) diff --git a/tests/test_cube.py b/tests/test_cube.py index 07db7a2..17bb8a1 100644 --- a/tests/test_cube.py +++ b/tests/test_cube.py @@ -19,16 +19,33 @@ from pathlib import Path +import pytest from pandas import DataFrame as PandasDataFrame from pysits.conversions.dsl.mask import MaskValue from pysits.models.data.cube import SITSCubeModel from pysits.models.data.frame import SITSFrame -from pysits.sits.cube import sits_cube, sits_reclassify +from pysits.sits.cube import ( + convert_reclassify_rules, + sits_cube, + sits_reclassify, + sits_texture, +) from pysits.sits.data import sits_bands, sits_bbox, sits_labels, sits_timeline from pysits.sits.utils import r_package_dir +@pytest.fixture(scope="module") +def local_cube() -> SITSCubeModel: + """Cube created from local files.""" + return sits_cube( + source="BDC", + collection="MOD13Q1-6.1", + data_dir=r_package_dir("extdata/raster/mod13q1", package="sits"), + progress=False, + ) + + def test_sits_cube_data_structure(): """Test data structure of sits_cube.""" # Define a region of interest for the city of Sinop @@ -141,7 +158,7 @@ def test_sits_cube_filter(): assert not isinstance(cube_tile2._instance, PandasDataFrame) -def s(tmp_path: Path): +def test_cube_reclassify(tmp_path: Path): """Test reclassify of classified cube.""" # Open mask map data_dir = r_package_dir("extdata/raster/prodes", package="sits") @@ -248,8 +265,61 @@ def s(tmp_path: Path): multicores=2, output_dir=tmp_path, version="ex_reclassify", + progress=False, ) assert isinstance(ro_mask, SITSCubeModel) assert ro_mask.shape[0] == 1 # noqa: PLR2004 - number of tiles assert len(sits_labels(ro_mask)) == 5 # noqa: PLR2004 - number of labels + + +def test_cube_reclassify_invalid_rules(): + """Test reclassify with rules defined in an invalid format.""" + with pytest.raises(ValueError, match="rules must be a dictionary"): + convert_reclassify_rules(["Water_Mask"]) + + +def test_cube_texture(tmp_path: Path, local_cube): + """Test texture measures of a data cube.""" + cube_texture = sits_texture( + local_cube, + NDVIVAR="glcm_variance(NDVI)", + window_size=5, + output_dir=tmp_path, + progress=False, + ) + + assert isinstance(cube_texture, SITSCubeModel) + assert sits_bands(cube_texture) == ["NDVI", "NDVIVAR"] + + +def test_cube_sync_instance(local_cube): + """Test sync of a modified cube with R.""" + cube = local_cube.copy() + classes = list(cube._instance.rclass) + + # Changing the cube marks it to be synced with R + cube["source"] = "BDC" + assert cube._is_updated + + # Using the cube in an operation syncs it, keeping its R classes + assert sits_bands(cube) == ["NDVI"] + assert list(cube._instance.rclass) == classes + + +def test_cube_sync_instance_without_rows(local_cube): + """Test sync of a cube without rows.""" + cube = local_cube.query("tile == 'unknown-tile'") + cube["source"] = "BDC" + + cube._sync_instance() + + assert cube._instance is None + + +def test_cube_html_representation(local_cube): + """Test cube HTML representation.""" + html = local_cube._repr_html_() + + assert local_cube.tile.iloc[0] in html + assert local_cube.collection.iloc[0] in html diff --git a/tests/test_data.py b/tests/test_data.py index 6e4dc92..66b87e9 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -19,13 +19,17 @@ from pathlib import Path +import pytest +from pandas import DataFrame as PandasDataFrame + from pysits.models.data.cube import SITSCubeModel from pysits.models.data.ts import SITSTimeSeriesModel -from pysits.sits.context import samples_l8_rondonia_2bands +from pysits.sits.context import samples_l8_rondonia_2bands, samples_modis_ndvi from pysits.sits.cube import sits_cube, sits_regularize from pysits.sits.data import ( sits_apply, sits_bands, + sits_labels, sits_merge, sits_reduce, sits_select, @@ -216,3 +220,32 @@ def test_cube_reduce(tmp_path: Path): # Check cube properties assert "NDVIMEAN" in sits_bands(cube_reduced) assert len(sits_timeline(cube_reduced)) == 1 # noqa: PLR2004 - one date + + +def test_frame_accessor_labels(): + """Test labels accessor in a sits data frame.""" + assert samples_modis_ndvi.sits.labels == sits_labels(samples_modis_ndvi) + + +def test_frame_accessor_labels_setter(): + """Test labels accessor setter in a sits data frame.""" + samples = samples_modis_ndvi.copy() + new_labels = ["Class_1", "Class_2", "Class_3", "Class_4"] + + samples.sits.labels = new_labels + + assert samples.sits.labels == new_labels + + # Original samples must not be changed + assert samples_modis_ndvi.sits.labels != new_labels + + +def test_frame_accessor_invalid_data(): + """Test labels accessor in a data frame without sits data.""" + data = PandasDataFrame({"a": [1, 2]}) + + with pytest.raises(ValueError, match="not a SITS data frame"): + data.sits.labels + + with pytest.raises(ValueError, match="not a SITS data frame"): + data.sits.labels = ["Class_1"] diff --git a/tests/test_indexing.py b/tests/test_indexing.py index 52890a1..784a8b9 100644 --- a/tests/test_indexing.py +++ b/tests/test_indexing.py @@ -17,74 +17,148 @@ """Unit tests for indexing operations.""" +import numpy as np +import pytest +import rasterio +from affine import Affine from pandas import Series as PandasSeries +from rasterio.crs import CRS from pysits.models.data.cube import SITSCubeItemModel, SITSCubeModel from pysits.models.data.ts import SITSTimeSeriesItemModel, SITSTimeSeriesModel from pysits.sits.context import samples_l8_rondonia_2bands from pysits.sits.cube import sits_cube +# +# Tiles of the cube used to test indexing +# +CUBE_TILES = ("20LLQ", "20LLR") -def test_cube_indexing(): - """Test cube indexing.""" - cbers_tile = sits_cube( - source="BDC", - collection="CBERS-WFI-16D", - bands=("NDVI", "EVI"), - tiles=("007004", "007005"), - start_date="2018-09-01", - end_date="2019-08-28", +# +# Grid of the cube used to test indexing +# +CUBE_CRS = "EPSG:32720" +CUBE_SIZE = 50 +CUBE_RESOLUTION = 10.0 + + +@pytest.fixture(scope="session") +def cube_dir(tmp_path_factory): + """Create a directory with files of two tiles.""" + # Create directory + data_dir = tmp_path_factory.mktemp("cube-tiles") + + # Create a raster file for each tile + for index, tile in enumerate(CUBE_TILES): + # Transform matrix for the tile + transform = Affine( + a=CUBE_RESOLUTION, + b=0.0, + c=300000.0 + index * 100000.0, + d=0.0, + e=-CUBE_RESOLUTION, + f=8000000.0, + ) + + # Create a raster file for each date + for date in ("2020-01-01", "2020-01-16"): + # Create a raster file for the date + with rasterio.open( + fp=data_dir / f"SENTINEL-2_MSI_{tile}_B02_{date}.tif", + mode="w", + driver="GTiff", + height=CUBE_SIZE, + width=CUBE_SIZE, + count=1, + dtype="int16", + crs=CRS.from_string(CUBE_CRS), + transform=transform, + nodata=-9999, + ) as dataset: + dataset.write(np.full((CUBE_SIZE, CUBE_SIZE), index + 1, "int16"), 1) + + return data_dir + + +@pytest.fixture +def cube(cube_dir): + """Create a cube with two tiles.""" + return sits_cube( + source="AWS", + collection="SENTINEL-2-L2A", + data_dir=cube_dir.as_posix(), + parse_info=("X1", "X2", "tile", "band", "date"), + bands="B02", + progress=False, ) + +def test_cube_indexing(cube): + """Test cube indexing.""" + assert sorted(cube.tile) == list(CUBE_TILES) + # Indexing tests - idx1 = cbers_tile[cbers_tile["tile"] == "007005"] + idx1 = cube[cube["tile"] == "20LLR"] assert idx1.shape[0] == 1 # noqa: PLR2004 - 1 row - assert idx1.tile.iloc[0] == "007005" + assert idx1.tile.iloc[0] == "20LLR" assert idx1._instance is not None assert isinstance(idx1, SITSCubeModel) - idx2 = cbers_tile.query("tile == '007004'") + idx2 = cube.query("tile == '20LLQ'") assert idx2.shape[0] == 1 # noqa: PLR2004 - 1 row - assert idx2.tile.iloc[0] == "007004" + assert idx2.tile.iloc[0] == "20LLQ" assert idx2._instance is not None assert isinstance(idx2, SITSCubeModel) - idx3 = cbers_tile.iloc[0] + idx3 = cube.iloc[0] assert idx3.shape[0] == 11 # noqa: PLR2004 - 11 columns - assert idx3.tile == "007004" + assert idx3.tile == "20LLQ" assert idx3._instance is not None assert isinstance(idx3, SITSCubeItemModel) - idx4 = cbers_tile.iloc[0:1,] + idx4 = cube.iloc[0:1,] assert idx4.shape[0] == 1 # noqa: PLR2004 - 1 row - assert idx4.tile.iloc[0] == "007004" + assert idx4.tile.iloc[0] == "20LLQ" assert idx4._instance is not None assert isinstance(idx4, SITSCubeModel) - idx5 = cbers_tile.iloc[0:1, 4] + idx5 = cube.iloc[0:1, 4] assert idx5.shape[0] == 1 # noqa: PLR2004 - 1 row - assert idx5.iloc[0] == "007004" + assert idx5.iloc[0] == "20LLQ" assert isinstance(idx5, PandasSeries) - idx6 = cbers_tile.loc[0] + idx6 = cube.loc[0] assert idx6.shape[0] == 11 # noqa: PLR2004 - 11 columns - assert idx6.tile == "007004" + assert idx6.tile == "20LLQ" assert idx6._instance is not None assert isinstance(idx6, SITSCubeItemModel) - idx7 = cbers_tile.loc[0:1,] - assert idx7.shape[0] == 2 # noqa: PLR2004 - 1 row + idx7 = cube.loc[0:1,] + assert idx7.shape[0] == 2 # noqa: PLR2004 - 2 rows assert idx7._instance is not None assert isinstance(idx7, SITSCubeModel) - idx8 = cbers_tile.loc[0, "tile"] - assert idx8 == "007004" + idx8 = cube.loc[0, "tile"] + assert idx8 == "20LLQ" cols = ["source", "collection", "tile"] - idx9 = cbers_tile[cols] + idx9 = cube[cols] assert [col in idx9.columns for col in cols] +def test_cube_indexing_without_results(cube): + """Test cube indexing when no rows are selected.""" + rows = (cube[cube["tile"] == "does-not-exist"], cube.query("tile == 'none'")) + + for empty in rows: + # Test types + assert isinstance(empty, SITSCubeModel) + assert empty.shape[0] == 0 + + # Cube instances are empty + assert empty._instance is None + + def test_ts_indexing(): """Test time-series indexing.""" samples = samples_l8_rondonia_2bands diff --git a/tests/test_models.py b/tests/test_ml_models.py similarity index 100% rename from tests/test_models.py rename to tests/test_ml_models.py diff --git a/tests/test_resolver.py b/tests/test_resolver.py new file mode 100644 index 0000000..d08878d --- /dev/null +++ b/tests/test_resolver.py @@ -0,0 +1,82 @@ +# +# Copyright (C) 2025 sits developers. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, see . +# + +"""Unit tests for class resolvers.""" + +import pytest +import rpy2.robjects as ro + +from pysits.models.data.accuracy import SITSAccuracy +from pysits.models.data.base import SITStructureData +from pysits.models.data.cube import SITSCubeModel +from pysits.models.data.frame import SITSFrame, SITSFrameSF +from pysits.models.data.matrix import SITSConfusionMatrix, SITSMatrix +from pysits.models.data.ts import ( + SITSTimeSeriesClassificationModel, + SITSTimeSeriesModel, + SITSTimeSeriesSFModel, +) +from pysits.models.data.tuning import SITSTuningResults +from pysits.models.ml import SITSMachineLearningMethod +from pysits.models.resolver import accuracy_class_resolver, content_class_resolver + + +def r_object_as(*classes: str) -> ro.vectors.DataFrame: + """Create an R object with the given classes.""" + data = ro.r("tibble::tibble(a = 1)") + data.rclass = ro.StrVector(classes) + + return data + + +@pytest.mark.parametrize( + ("classes", "expected_class"), + [ + (("predicted", "sits", "tbl_df"), SITSTimeSeriesClassificationModel), + (("sits", "tbl_df"), SITSTimeSeriesModel), + (("raster_cube", "tbl_df"), SITSCubeModel), + (("sits_tuned", "tbl_df"), SITSTuningResults), + (("sf", "tbl_df"), SITSTimeSeriesSFModel), + (("sf", "data.frame"), SITSFrameSF), + (("tbl_df", "data.frame"), SITSFrame), + (("matrix", "array"), SITSMatrix), + (("sits_model", "function"), SITSMachineLearningMethod), + (("som_map", "list"), SITStructureData), + ], +) +def test_content_class_resolver(classes, expected_class): + """Test content class resolution.""" + assert content_class_resolver(r_object_as(*classes)) is expected_class + + +@pytest.mark.parametrize( + ("classes", "expected_class"), + [ + (("confusionMatrix",), SITSConfusionMatrix), + (("sits_area_accuracy", "list"), SITSAccuracy), + ], +) +def test_accuracy_class_resolver(classes, expected_class): + """Test accuracy class resolution.""" + assert accuracy_class_resolver(r_object_as(*classes)) is expected_class + + +@pytest.mark.parametrize("resolver", [content_class_resolver, accuracy_class_resolver]) +def test_class_resolver_unsupported_object(resolver): + """Test class resolution of an unsupported R object.""" + with pytest.raises(ValueError, match="Unknown or unsupported R object"): + resolver(ro.r("1:3")) diff --git a/tests/test_rl_models.py b/tests/test_rl_models.py new file mode 100644 index 0000000..955cff1 --- /dev/null +++ b/tests/test_rl_models.py @@ -0,0 +1,81 @@ +# +# Copyright (C) 2025 sits developers. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, see . +# + +"""Unit tests for representation learning models.""" + +import pytest + +from pysits.models.ml import SITSRepresentationLearningMethod +from pysits.sits.context import samples_modis_ndvi +from pysits.sits.ml import ( + sits_barlow_twins, + sits_contrastive_learning, + sits_lighttae, + sits_pre_train, + sits_ssl_lejepa, + sits_ssl_mae, + sits_ssl_vicreg, +) + +# +# Encoder methods available to test +# +ALL_ENCODER_METHODS = [ + sits_ssl_mae, + sits_ssl_lejepa, + sits_ssl_vicreg, + sits_contrastive_learning, + sits_barlow_twins, +] + + +# +# Test pre-training for all available encoder methods +# +@pytest.mark.parametrize("model_fn", ALL_ENCODER_METHODS) +def test_model_pre_training(model_fn): + """Test pre-training for all available encoder methods.""" + try: + # Create encoder method + rl_method = model_fn(epochs=1) + + # Pre-train model + model = sits_pre_train( + samples=samples_modis_ndvi, + rl_method=rl_method, + ) + + # Test! + assert model is not None + assert isinstance(model, SITSRepresentationLearningMethod) + + except Exception as e: + pytest.fail(f"Pre-training failed: {str(e)}") + + +@pytest.mark.parametrize("model_fn", ALL_ENCODER_METHODS) +def test_model_pre_training_with_encoder_model(model_fn): + """Test pre-training with a user-defined encoder model.""" + model = sits_pre_train( + samples=samples_modis_ndvi, + rl_method=model_fn( + encoder_model=sits_lighttae(), + epochs=1, + ), + ) + + assert isinstance(model, SITSRepresentationLearningMethod) diff --git a/tests/test_tiles.py b/tests/test_tiles.py index 145bf5f..8654095 100644 --- a/tests/test_tiles.py +++ b/tests/test_tiles.py @@ -17,26 +17,64 @@ """Unit tests for tile-related operations.""" -from pysits import sits_cube, sits_mgrs_to_roi, sits_tiles_to_roi +import pytest + +from pysits import sits_cube, sits_roi_to_tiles, sits_tiles_to_roi from pysits.models.data.cube import SITSCubeModel +from pysits.models.data.frame import SITSFrameSF from pysits.models.data.vector import SITSNamedVector -def test_tiles_to_roi(): +@pytest.mark.parametrize( + ("grid_system", "expected_tiles"), + [ + ("MGRS", ["20LLQ", "20LMQ"]), + ("BDC_MD_V2", ["006007", "006008"]), + ], +) +def test_roi_to_tiles(grid_system: str, expected_tiles: list[str]): + """Test ROI to tiles.""" + roi = dict( + lon_min=-64.037, + lat_min=-9.644, + lon_max=-63.886, + lat_max=-9.389, + ) + + tiles = sits_roi_to_tiles(roi, grid_system=grid_system) + + # Test type + assert isinstance(tiles, SITSFrameSF) + + # Columns + assert all( + x in tiles.columns for x in ["tile_id", "coverage_percentage", "geometry"] + ) + + # Expected tiles + assert sorted(tiles["tile_id"]) == expected_tiles + + +@pytest.mark.parametrize( + ("grid_system", "tiles", "expected_roi"), + [ + ("MGRS", "22KGA", (-49.067207, -22.683553, -47.985736, -21.676399)), + ("BDC_MD_V2", "006007", (-64.242845, -9.529926, -62.235677, -7.584190)), + ], +) +def test_tiles_to_roi(grid_system: str, tiles: str, expected_roi: tuple[float, ...]): """Test tiles to ROI.""" - # Test new version - roi = sits_tiles_to_roi("22KGA") + roi = sits_tiles_to_roi(tiles, grid_system=grid_system) - assert roi.shape == (1, 4) + # Test type assert isinstance(roi, SITSNamedVector) - assert all(x in roi.columns for x in ["xmin", "xmax", "ymin", "ymax"]) - # Test deprecated version - roi2 = sits_mgrs_to_roi("20LMM") + # Columns + assert roi.shape == (1, 4) + assert all(x in roi.columns for x in ["lon_min", "lat_min", "lon_max", "lat_max"]) - assert roi2.shape == (1, 4) - assert isinstance(roi2, SITSNamedVector) - assert all(x in roi2.columns for x in ["xmin", "xmax", "ymin", "ymax"]) + # Expected ROI + assert roi.iloc[0].tolist() == pytest.approx(expected_roi) def test_tiles_to_load_cube(): @@ -54,3 +92,30 @@ def test_tiles_to_load_cube(): assert isinstance(cube, SITSCubeModel) assert cube.tile.iloc[0] == "013011" + + +def test_roi_to_tiles_to_load_cube(): + """Test ROI tiles to load cube.""" + roi = dict( + lon_min=-64.037, + lat_min=-9.644, + lon_max=-63.886, + lat_max=-9.389, + ) + + # Find tiles of the ROI + tiles = sits_roi_to_tiles(roi, grid_system="MGRS") + + # Load cube using the tiles found + cube = sits_cube( + source="AWS", + collection="SENTINEL-2-L2A", + tiles=tiles["tile_id"].tolist(), + bands=("B02",), + start_date="2020-01-01", + end_date="2020-02-01", + progress=False, + ) + + assert isinstance(cube, SITSCubeModel) + assert sorted(cube.tile) == ["20LLQ", "20LMQ"] diff --git a/tests/test_tuning.py b/tests/test_tuning.py new file mode 100644 index 0000000..57f353c --- /dev/null +++ b/tests/test_tuning.py @@ -0,0 +1,106 @@ +# +# Copyright (C) 2025 sits developers. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, see . +# + +"""Unit tests for tuning operations.""" + +import pytest + +from pysits.conversions.dsl.tuning import hparam +from pysits.models.data.tuning import SITSTuningResults +from pysits.sits.context import samples_modis_ndvi +from pysits.sits.ml import sits_tempcnn +from pysits.sits.tuning import sits_tuning, sits_tuning_hparams + +# +# Hyper-parameters options available for tuning +# +CNN_LAYERS = [[128.0, 128.0, 128.0], [64.0, 64.0, 64.0]] +CNN_KERNELS = [[3.0, 3.0, 3.0], [5.0, 5.0, 5.0]] +BETAS = [[0.9, 0.999], [0.85, 0.99]] +OPTIMIZERS = ["optim_adamw", "optim_adam"] + +# +# Number of trials used in the tests +# +TRIALS = 2 + + +@pytest.fixture(scope="module") +def tuned_tempcnn() -> SITSTuningResults: + """Tuning results of a ``sits_tempcnn`` model.""" + return sits_tuning( + samples=samples_modis_ndvi, + ml_method=sits_tempcnn, + params=sits_tuning_hparams( + cnn_layers=hparam("choice", *[tuple(x) for x in CNN_LAYERS]), + cnn_kernels=hparam("choice", *[tuple(x) for x in CNN_KERNELS]), + epochs=1, + optimizer=hparam("choice", *[f"torch::{x}" for x in OPTIMIZERS]), + opt_hparams=dict( + lr=hparam("loguniform", 10**-2, 10**-4), + betas=hparam("choice", *[tuple(x) for x in BETAS]), + ), + ), + trials=TRIALS, + multicores=1, + ) + + +def test_tuning_metrics(tuned_tempcnn): + """Test tuning metrics results.""" + assert isinstance(tuned_tempcnn, SITSTuningResults) + + assert len(tuned_tempcnn.accuracy) == TRIALS + assert len(tuned_tempcnn.kappa) == TRIALS + assert len(tuned_tempcnn.acc) == TRIALS + + +def test_tuning_vector_hparams(tuned_tempcnn): + """Test tuning results of hyper-parameters defined as vectors.""" + assert len(tuned_tempcnn.cnn_layers) == TRIALS + assert len(tuned_tempcnn.cnn_kernels) == TRIALS + + for layers, kernels in zip(tuned_tempcnn.cnn_layers, tuned_tempcnn.cnn_kernels): + assert layers in CNN_LAYERS + assert kernels in CNN_KERNELS + + +def test_tuning_scalar_hparams(tuned_tempcnn): + """Test tuning results of hyper-parameters defined as scalars.""" + assert tuned_tempcnn.epochs == [[1.0]] * TRIALS + assert tuned_tempcnn.validation_split == [[0.2]] * TRIALS + assert tuned_tempcnn.verbose == [[False]] * TRIALS + + +def test_tuning_optimizer_hparams(tuned_tempcnn): + """Test tuning results of optimizer hyper-parameters.""" + assert len(tuned_tempcnn.optimizer) == TRIALS + + for optimizer in tuned_tempcnn.optimizer: + assert optimizer in OPTIMIZERS + + for opt_hparams in tuned_tempcnn.opt_hparams: + assert len(opt_hparams["lr"]) == 1 + assert opt_hparams["betas"] in BETAS + + +def test_tuning_html_representation(tuned_tempcnn): + """Test tuning HTML representation.""" + html = tuned_tempcnn._repr_html_() + + for layers in tuned_tempcnn.cnn_layers: + assert str(layers) in html diff --git a/tests/test_validation.py b/tests/test_validation.py index 6924272..a070e2e 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -17,13 +17,54 @@ """Unit tests for validation operations (cube and time-series).""" +import pytest +import rpy2.robjects as ro +from pandas import DataFrame as PandasDataFrame + +from pysits.models.data.accuracy import SITSAccuracy from pysits.models.data.matrix import SITSConfusionMatrix from pysits.models.data.table import SITSTable from pysits.models.data.vector import SITSNamedVector +from pysits.models.resolver import resolve_and_invoke_accuracy_class from pysits.sits.context import cerrado_2classes from pysits.sits.ml import sits_rfor from pysits.sits.ts import sits_sample, sits_validate +# +# Area accuracy results, as produced by ``sits_accuracy`` on a classified cube +# +R_AREA_ACCURACY = """ + local({ + labels <- c("Forest", "Pasture") + + accuracy <- list( + error_matrix = table( + factor(c("Forest", "Forest", "Pasture"), levels = labels), + factor(c("Forest", "Pasture", "Pasture"), levels = labels) + ), + area_pixels = c(Forest = 100, Pasture = 200), + error_ajusted_area = c(Forest = 150, Pasture = 150), + stderr_prop = c(Forest = 0.1, Pasture = 0.2), + stderr_area = c(Forest = 10, Pasture = 20), + conf_interval = c(Forest = 19.6, Pasture = 39.2), + accuracy = list( + user = c(Forest = 0.5, Pasture = 1.0), + producer = c(Forest = 1.0, Pasture = 0.5), + overall = 0.75 + ) + ) + + class(accuracy) <- c("sits_area_accuracy", class(accuracy)) + accuracy + }) +""" + + +@pytest.fixture +def area_accuracy() -> SITSAccuracy: + """Area accuracy results.""" + return resolve_and_invoke_accuracy_class(ro.r(R_AREA_ACCURACY)) + def test_sits_validate(): """Test validate operation.""" @@ -48,3 +89,49 @@ def test_sits_validate(): # Check values assert matrix.mode == "sens_spec" assert matrix.positive == "Cerrado" + + +def test_area_accuracy_matrix(area_accuracy): + """Test area accuracy error matrix.""" + assert isinstance(area_accuracy, SITSAccuracy) + + error_matrix = area_accuracy.error_matrix + + assert isinstance(error_matrix, SITSTable) + assert error_matrix.index.tolist() == ["Forest", "Pasture"] + assert error_matrix["Forest"].tolist() == [1, 0] + + +def test_area_accuracy_areas(area_accuracy): + """Test area accuracy area properties.""" + for name in ( + "area_pixels", + "error_ajusted_area", + "stderr_prop", + "stderr_area", + "conf_interval", + ): + value = getattr(area_accuracy, name) + + assert isinstance(value, SITSNamedVector) + assert value.columns.tolist() == ["Forest", "Pasture"] + + assert area_accuracy.area_pixels["Pasture"].tolist() == [200.0] + assert area_accuracy.stderr_prop["Forest"].tolist() == [0.1] + + +def test_area_accuracy_metrics(area_accuracy): + """Test area accuracy user, producer and overall metrics.""" + accuracy = area_accuracy.accuracy + + assert accuracy["overall"] == [0.75] + assert isinstance(accuracy["user"], PandasDataFrame) + assert isinstance(accuracy["producer"], PandasDataFrame) + assert accuracy["user"]["Forest"].tolist() == [0.5] + assert accuracy["producer"]["Forest"].tolist() == [1.0] + + +def test_area_accuracy_representation(area_accuracy): + """Test area accuracy string representation.""" + assert "Area Weighted Statistics" in str(area_accuracy) + assert str(area_accuracy) == repr(area_accuracy) diff --git a/tests/test_visualization.py b/tests/test_visualization.py index 8d18646..e11d8e7 100644 --- a/tests/test_visualization.py +++ b/tests/test_visualization.py @@ -19,7 +19,7 @@ from pysits.sits.context import samples_l8_rondonia_2bands from pysits.sits.cube import sits_cube -from pysits.sits.ml import sits_rfor, sits_train +from pysits.sits.ml import sits_pre_train, sits_rfor, sits_ssl_mae, sits_train from pysits.sits.ts import sits_patterns, sits_som_map from pysits.sits.utils import r_package_dir from pysits.sits.visualization import sits_plot, sits_view @@ -42,6 +42,12 @@ def test_machine_learning_visualization(no_plot_window): sits_plot(ml_model) +def test_representation_learning_visualization(no_plot_window): + """Test representation learning visualization.""" + rl_model = sits_pre_train(samples_l8_rondonia_2bands, sits_ssl_mae()) + sits_plot(rl_model) + + def test_som_visualization(no_plot_window): """Test SOM visualization.""" som = sits_som_map(data=samples_l8_rondonia_2bands) diff --git a/tests/test_xarray.py b/tests/test_xarray.py index b97c532..4ef7419 100644 --- a/tests/test_xarray.py +++ b/tests/test_xarray.py @@ -17,36 +17,481 @@ """Unit tests for xarray export operations.""" +import sys + +import numpy as np +import pytest +import rasterio import xarray as xr +from affine import Affine +from dask.array import Array as DaskArray +from rasterio.crs import CRS +from rasterio.windows import Window from pysits.sits.context import samples_l8_rondonia_2bands from pysits.sits.cube import sits_cube +from pysits.sits.data import sits_bbox from pysits.sits.exporters.xarray import sits_as_xarray from pysits.sits.utils import r_package_dir +# +# Size and block size of the cube created to test how data is read +# +COG_SIZE = 4000 +COG_BLOCK = 512 -def test_xarray_cube_conversion(): - """Test xarray cube conversion.""" - # Create a data cube from local files - data_dir = r_package_dir("extdata/raster/mod13q1", package="sits") - cube = sits_cube( +# +# Grid of the cube created to test how data is read +# +COG_CRS = "EPSG:32620" +COG_RESOLUTION = 10.0 +COG_TRANSFORM = Affine( + a=COG_RESOLUTION, + b=0.0, + c=300000.0, + d=0.0, + e=-COG_RESOLUTION, + f=8000000.0, +) + +# +# Dates of the cube created to test how data is read +# +COG_DATES = ("2020-01-01", "2020-01-16") + + +# +# Auxiliary functions +# +def _mod13q1_cube(): + """Create a cube from the local MOD13Q1 files.""" + return sits_cube( source="BDC", collection="MOD13Q1-6.1", - data_dir=data_dir, + data_dir=r_package_dir("extdata/raster/mod13q1", package="sits"), + progress=False, + ) + + +def _class_cube(): + """Create a cube from the local classification files.""" + return sits_cube( + source="MPC", + collection="SENTINEL-2-L2A", + data_dir=r_package_dir("extdata/raster/classif", package="sits"), + parse_info=("X1", "X2", "tile", "start_date", "end_date", "band", "version"), + bands="class", + labels={ + "1": "ClearCut_Fire", + "2": "ClearCut_Soil", + "3": "ClearCut_Veg", + "4": "Forest", + }, + progress=False, + ) + + +def _probs_cube(labels=None): + """Create a cube from the local probability files.""" + return sits_cube( + source="BDC", + collection="MOD13Q1-6.1", + data_dir=r_package_dir("extdata/raster/probs", package="sits"), + parse_info=("tile", "X2", "band", "start_date", "end_date", "version"), + bands="probs", + labels=labels or {str(i): f"class_{i}" for i in range(1, 10)}, + progress=False, + ) + + +def _first_cube_file(cube): + """Get the first file of a cube.""" + files = cube["file_info"].tolist()[0] + files = files.sort_values(files.columns[1]).reset_index(drop=True) + + return str(files.iloc[0]["path"]) + + +class _ReadCounter: + """Count how much data is read from files.""" + + def __init__(self, monkeypatch): + """Initializer.""" + self.opens = [] + self.pixels = 0 + + original_open = rasterio.open + original_read = rasterio.DatasetReader.read + + def counting_open(*args, **kwargs): + self.opens.append(str(args[0]) if args else "") + + return original_open(*args, **kwargs) + + def counting_read(reader, *args, **kwargs): + values = original_read(reader, *args, **kwargs) + + if np.ndim(values) >= 2: # noqa: PLR2004 + self.pixels += int(np.shape(values)[-1] * np.shape(values)[-2]) + + return values + + monkeypatch.setattr(rasterio, "open", counting_open) + monkeypatch.setattr(rasterio.DatasetReader, "read", counting_read) + + def reset(self): + """Reset counters.""" + self.opens = [] + self.pixels = 0 + + +@pytest.fixture(scope="session") +def cog_cube_dir(tmp_path_factory): + """Create a directory.""" + data_dir = tmp_path_factory.mktemp("cog-cube") + + for index, date in enumerate(COG_DATES): + values = ( + np.arange(COG_SIZE * COG_SIZE).reshape(COG_SIZE, COG_SIZE) + index * 7 + ) % 10000 + + # Value as int16 + values = values.astype("int16") + + with rasterio.open( + fp=data_dir / f"SENTINEL-2_MSI_20LLQ_B02_{date}.tif", + mode="w", + driver="GTiff", + height=COG_SIZE, + width=COG_SIZE, + count=1, + dtype="int16", + crs=CRS.from_string(COG_CRS), + transform=COG_TRANSFORM, + nodata=-9999, + tiled=True, + blockxsize=COG_BLOCK, + blockysize=COG_BLOCK, + ) as dataset: + dataset.write(values, 1) + + return data_dir + + +@pytest.fixture +def cog_cube(cog_cube_dir): + """Create a cube.""" + return sits_cube( + source="AWS", + collection="SENTINEL-2-L2A", + data_dir=cog_cube_dir.as_posix(), + parse_info=("X1", "X2", "tile", "band", "date"), + bands="B02", + progress=False, + ) + + +# +# Data cubes +# +def test_xarray_cube_dimensions(): + """Test cube conversion dimensions and coordinates.""" + data = sits_as_xarray(_mod13q1_cube()) + + assert isinstance(data, xr.DataArray) + assert data.dims == ("band", "time", "y", "x") + + # Shape comes from the cube files + assert data.sizes == {"band": 1, "time": 12, "y": 147, "x": 255} + assert list(data["band"].values) == ["NDVI"] + + # Band configuration is available to users + assert data["scale_factor"].item() == pytest.approx(0.0001) + assert data["missing_value"].item() == -3000 # noqa: PLR2004 + + # Data is georeferenced + assert data.rio.crs is not None + assert "spatial_ref" in data.coords + + +def test_xarray_cube_values(): + """Test cube conversion values, against the values in the files.""" + cube = _mod13q1_cube() + data = sits_as_xarray(cube) + + with rasterio.open(_first_cube_file(cube)) as dataset: + expected = dataset.read(1).astype("float32") + expected_transform = dataset.transform + + # Missing values are removed, and values are scaled + expected = np.where(expected == -3000, np.nan, expected) * 0.0001 # noqa: PLR2004 + + # Get NDVI value + values = data.sel(band="NDVI").isel(time=0).compute() + + # Test type and values + assert values.dtype == np.dtype("float32") + assert np.allclose(values.values, expected, equal_nan=True) + + # Values are in their physical range + assert -1 <= float(values.min()) <= float(values.max()) <= 1 + + # Data is aligned with the files + assert data.rio.transform().c == pytest.approx(expected_transform.c) + assert data.rio.transform().f == pytest.approx(expected_transform.f) + + +def test_xarray_cube_without_scale(): + """Test cube conversion without scaling.""" + cube = _mod13q1_cube() + data = sits_as_xarray(cube, scale=False) + + with rasterio.open(_first_cube_file(cube)) as dataset: + expected = dataset.read(1) + + # Test with NDVI + values = data.sel(band="NDVI").isel(time=0).compute() + + # Values keep their original type and range + assert values.dtype == expected.dtype + assert np.array_equal(values.values, expected) + + +def test_xarray_cube_bands(): + """Test cube conversion with band selection.""" + cube = _mod13q1_cube() + data = sits_as_xarray(cube, bands=["NDVI"]) + + assert list(data["band"].values) == ["NDVI"] + + with pytest.raises(ValueError, match="Bands not available"): + sits_as_xarray(cube, bands=["EVI"]) + + +# +# Lazy access +# +def test_xarray_cube_is_lazy(cog_cube, monkeypatch): + """Test that cube conversion doesn't read data from files.""" + counter = _ReadCounter(monkeypatch) + + data = sits_as_xarray(cog_cube) + + # Building the cube uses only metadata + assert counter.opens == [] + assert counter.pixels == 0 + + # Data is available as chunks, and is read only when required + assert isinstance(data.data, DaskArray) + assert data.chunksizes["x"][0] == COG_BLOCK + assert data.chunksizes["y"][0] == COG_BLOCK + + +def test_xarray_cube_reads_windows(cog_cube, monkeypatch): + """Test that only the required data is read from files.""" + counter = _ReadCounter(monkeypatch) + + # Transform + data = sits_as_xarray(cog_cube, scale=False) + + # Reset + counter.reset() + + # Select + values = data.isel( + band=0, + time=0, + y=slice(0, 10), + x=slice(0, 10), + ) + values = values.compute() + + # Values must be small block + assert len(counter.opens) == 1 + assert counter.pixels <= COG_BLOCK * COG_BLOCK + assert counter.pixels < COG_SIZE * COG_SIZE + + # Load window + with rasterio.open(_first_cube_file(cog_cube)) as dataset: + expected = dataset.read(1, window=Window(0, 0, 10, 10)) + + # Both must be the same + assert np.array_equal(values.values, expected) + + +def test_xarray_cube_chunks(cog_cube): + """Test cube conversion with user-defined chunks.""" + data = sits_as_xarray(cog_cube, chunks={"x": 1024, "y": 1024}) + + assert data.chunksizes["x"][0] == 1024 # noqa: PLR2004 + assert data.chunksizes["y"][0] == 1024 # noqa: PLR2004 + + +# +# Derived cubes +# +def test_xarray_class_cube(): + """Test class cube conversion.""" + cube = _class_cube() + data = sits_as_xarray(cube) + + # Test metadata + assert isinstance(data, xr.DataArray) + assert data.dims == ("y", "x") + + # Test data properties + assert data.dtype == np.dtype("uint8") + assert data.attrs["labels"] == { + 1: "ClearCut_Fire", + 2: "ClearCut_Soil", + 3: "ClearCut_Veg", + 4: "Forest", + } + + # Load data + with rasterio.open(_first_cube_file(cube)) as dataset: + expected = dataset.read(1) + + # Values must be the same + assert np.array_equal(data.compute().values, expected) + + # Derived cubes describe a period + assert data.attrs["start_date"] == "2020-06-04" + assert data.attrs["end_date"] == "2021-08-26" + + +def test_xarray_probs_cube(): + """Test probability cube conversion.""" + data = sits_as_xarray(_probs_cube()) + + # Test type and properties + assert isinstance(data, xr.DataArray) + assert data.dims == ("label", "y", "x") + assert list(data["label"].values) == [f"class_{i}" for i in range(1, 10)] + + # Probabilities are scaled + assert data.dtype == np.dtype("float32") + assert 0 <= float(data.min()) <= float(data.max()) <= 1 + + +def test_xarray_probs_cube_invalid_labels(): + """Test probability cube conversion with invalid labels.""" + cube = _probs_cube(labels={"1": "Cerrado", "2": "Forest"}) + + with pytest.raises(ValueError, match="labels"): + sits_as_xarray(cube) + + +def test_xarray_derived_cube_bands(): + """Test that bands are not available for derived cubes.""" + with pytest.raises(ValueError, match="derived from a classification"): + sits_as_xarray(_class_cube(), bands=["class"]) + + +# +# Time series +# +def test_xarray_time_series(): + """Test time series conversion.""" + data = sits_as_xarray(samples_l8_rondonia_2bands) + + # Test type and properties + assert isinstance(data, xr.Dataset) + assert data.sizes == {"sample": 160, "time": 25} + assert all(band in data for band in ["EVI", "NDVI"]) + + # Sample metadata is available to users + assert all( + coord in data.coords for coord in ["longitude", "latitude", "label", "cube"] + ) + + +def test_xarray_time_series_bands(): + """Test time series conversion with band selection.""" + data = sits_as_xarray(samples_l8_rondonia_2bands, bands=["NDVI"]) + + # Ensure bands are correctly selected + assert "NDVI" in data + assert "EVI" not in data + + # Invalid band must produce an error + with pytest.raises(ValueError, match="Bands not available"): + sits_as_xarray(samples_l8_rondonia_2bands, bands=["B02"]) + + +def test_xarray_time_series_different_timelines(): + """Test time series conversion with samples of different timelines.""" + samples = samples_l8_rondonia_2bands.copy() + + # Remove a date from the first sample + time_series = samples["time_series"].tolist() + time_series[0] = time_series[0].iloc[1:].reset_index(drop=True) + samples["time_series"] = time_series + + # Transform + data = sits_as_xarray(samples) + + # Samples are aligned by date, and dates without data are empty + assert data.sizes == {"sample": 160, "time": 25} + assert bool(np.isnan(data["NDVI"].isel(sample=0, time=0))) + assert not bool(np.isnan(data["NDVI"].isel(sample=1, time=0))) + + +def test_xarray_time_series_cube_arguments(): + """Test that cube arguments are not available for time series.""" + with pytest.raises(ValueError, match="only available for data cubes"): + sits_as_xarray(samples_l8_rondonia_2bands, chunks={"x": 512}) + + with pytest.raises(ValueError, match="only available for data cubes"): + sits_as_xarray(samples_l8_rondonia_2bands, cube_args={"resampling": "bilinear"}) + + +# +# Errors +# +def test_xarray_unsupported_data(): + """Test conversion of data without an xarray representation.""" + with pytest.raises(NotImplementedError, match="sits_as_xarray"): + sits_as_xarray(sits_bbox(samples_l8_rondonia_2bands)) + + +def test_xarray_without_dependencies(monkeypatch): + """Test conversion when the xarray dependencies are not installed.""" + monkeypatch.setitem(sys.modules, "pysits.conversions.xarray", None) + + with pytest.raises(ImportError, match=r"pysits\[xarray\]"): + sits_as_xarray(samples_l8_rondonia_2bands) + + +# +# Cubes with multiple grids +# +def test_xarray_cube_multiple_grids(): + """Test conversion of a cube with bands in different resolutions.""" + cube = sits_cube( + source="AWS", + collection="SENTINEL-2-L2A", + tiles="20LLQ", + bands=("B02", "B11"), + start_date="2020-01-01", + end_date="2020-01-20", + progress=False, ) - xcube = sits_as_xarray(cube) + data = sits_as_xarray(cube) - assert isinstance(xcube, xr.Dataset) - assert xcube.time.size == 12 # noqa: PLR2004 - assert "NDVI" in xcube + # Bands in different grids are not resampled: each grid is a node + assert isinstance(data, xr.DataTree) + assert sorted(data.children) == ["epsg32720-10m", "epsg32720-20m"] + # Get bands + band_10m = data["epsg32720-10m"].ds["cube"] + band_20m = data["epsg32720-20m"].ds["cube"] -def test_xarray_sits_conversion(): - """Test xarray sits conversion.""" - xsamples_ts = sits_as_xarray(samples_l8_rondonia_2bands) + # Test properties + assert list(band_10m["band"].values) == ["B02"] + assert list(band_20m["band"].values) == ["B11"] - assert "EVI" in xsamples_ts - assert "NDVI" in xsamples_ts - assert xsamples_ts.time.size == 25 # noqa: PLR2004 - assert isinstance(xsamples_ts, xr.Dataset) + # Each node keeps the native grid of its files + assert band_10m.sizes["x"] == 10980 # noqa: PLR2004 + assert band_20m.sizes["x"] == 5490 # noqa: PLR2004 diff --git a/uv.lock b/uv.lock index 9ac5c37..537ad4f 100644 --- a/uv.lock +++ b/uv.lock @@ -16,6 +16,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0b/f7/85273299ab57117850cc0a936c64151171fac4da49bc6fba0dad984a7c5f/affine-2.4.0-py3-none-any.whl", hash = "sha256:8a3df80e2b2378aef598a83c1392efd47967afec4242021a0b06b4c7cbc61a92", size = 15662, upload-time = "2023-01-19T23:44:28.833Z" }, ] +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] + [[package]] name = "attrs" version = "25.3.0" @@ -25,6 +34,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, ] +[[package]] +name = "cachetools" +version = "7.1.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/d2/47e8bc06fe2a06d3f5bdf20f1126ab66c4e99dc48d940e7ba873f7ac7131/cachetools-7.1.7.tar.gz", hash = "sha256:a3e2a00b14d8f8a6b70c1dae7b4685e7ad3bc965c5b42124a2d6ce895da6cf50", size = 40680, upload-time = "2026-08-01T21:20:40.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/d8/767faeda872075724b95dd675466a645f1b92aadcdcf2d1429dcfd76c176/cachetools-7.1.7-py3-none-any.whl", hash = "sha256:ef98ef375ad188819ef2f9b3645e3987f4b8c5b7550e436ad998c2de78296df0", size = 16830, upload-time = "2026-08-01T21:20:38.977Z" }, +] + [[package]] name = "certifi" version = "2025.4.26" @@ -100,6 +118,93 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, ] +[[package]] +name = "charset-normalizer" +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/81/8e983840c6e5b93b33c2ba81aa3d52c2e42f0e9a690ce7607a2e61da4a5c/charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a", size = 322240, upload-time = "2026-07-07T14:32:36.236Z" }, + { url = "https://files.pythonhosted.org/packages/de/d1/b4319dc3229d8272fba305e206fc0a148e2de8d4087917ce62ae6382f359/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616", size = 216475, upload-time = "2026-07-07T14:32:38.142Z" }, + { url = "https://files.pythonhosted.org/packages/80/33/6c99c1b3e6b8bf730e1bc809b9a2608f224145069114c479a2e9e1494346/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209", size = 238670, upload-time = "2026-07-07T14:32:39.658Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f4/ffbb83546e1f198ecc70ecd372b65cf2b50f9068b380abd67640f17a8e18/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99", size = 233476, upload-time = "2026-07-07T14:32:41.155Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5f/b98b8da398637b551e427e7be922bdec19177dc54d6811dcdaa503f23aac/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8", size = 223817, upload-time = "2026-07-07T14:32:42.592Z" }, + { url = "https://files.pythonhosted.org/packages/36/31/a276bb2e66243072a3fd06fdcab9cbb61a305b02143d70d2bda21d888fa8/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b", size = 207974, upload-time = "2026-07-07T14:32:44.258Z" }, + { url = "https://files.pythonhosted.org/packages/5e/be/7ee4453d7e88dfbc4104ccd34900b9f2c7c17dac22881865fe0e82424a25/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2", size = 221655, upload-time = "2026-07-07T14:32:45.64Z" }, + { url = "https://files.pythonhosted.org/packages/1d/85/181c652953eb5276d198f375b1dd641047392050098100a3a02d6534f657/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9", size = 219229, upload-time = "2026-07-07T14:32:47.376Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e7/aaf6da33fc9f4691cda8f7efbc9f69179d3d39ec8a4799baf273ee1d8db0/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15", size = 209704, upload-time = "2026-07-07T14:32:48.855Z" }, + { url = "https://files.pythonhosted.org/packages/63/01/f2fb3bd3a73be48b173ee0c6aa8d2497af97d5663a8c4c4b491de4c62f7a/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d", size = 226243, upload-time = "2026-07-07T14:32:50.239Z" }, + { url = "https://files.pythonhosted.org/packages/c4/02/c57a22739fe05246b0b5783b3bfb6afaac4eebb46f3ececdfb2f048f780e/charset_normalizer-3.4.9-cp310-cp310-win32.whl", hash = "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381", size = 150935, upload-time = "2026-07-07T14:32:51.676Z" }, + { url = "https://files.pythonhosted.org/packages/37/8d/ca39a7559a4797505530d084fd3a49a2c959efbbbff146302fb7be4e3b35/charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee", size = 162314, upload-time = "2026-07-07T14:32:53.193Z" }, + { url = "https://files.pythonhosted.org/packages/01/da/a44bd7a13d426e69e4894557106cd58669097bfad4a8681123b618fbfc5d/charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419", size = 153075, upload-time = "2026-07-07T14:32:54.554Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", size = 317075, upload-time = "2026-07-07T14:32:56.021Z" }, + { url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837, upload-time = "2026-07-07T14:32:57.78Z" }, + { url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503, upload-time = "2026-07-07T14:32:59.205Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944, upload-time = "2026-07-07T14:33:00.803Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276, upload-time = "2026-07-07T14:33:02.199Z" }, + { url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260, upload-time = "2026-07-07T14:33:03.698Z" }, + { url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786, upload-time = "2026-07-07T14:33:05.12Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798, upload-time = "2026-07-07T14:33:06.629Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429, upload-time = "2026-07-07T14:33:08.006Z" }, + { url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066, upload-time = "2026-07-07T14:33:09.783Z" }, + { url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456, upload-time = "2026-07-07T14:33:11.217Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410, upload-time = "2026-07-07T14:33:12.743Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649, upload-time = "2026-07-07T14:33:14.173Z" }, + { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, + { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, + { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, + { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, + { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, + { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" }, + { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" }, + { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" }, + { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, + { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, + { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, + { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, + { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, + { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, + { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, +] + [[package]] name = "click" version = "8.2.1" @@ -318,6 +423,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/30/53b0844a7a4c6b041b111b24ca15cc9b8661a86fe1f6aaeb2d0d7f0fb1f2/dask-2025.5.1-py3-none-any.whl", hash = "sha256:3b85fdaa5f6f989dde49da6008415b1ae996985ebdfb1e40de2c997d9010371d", size = 1474226, upload-time = "2025-05-20T19:54:20.309Z" }, ] +[package.optional-dependencies] +array = [ + { name = "numpy" }, +] + [[package]] name = "distlib" version = "0.3.9" @@ -424,6 +534,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7a/cd/18f8da995b658420625f7ef13f037be53ae04ec5ad33f9b718240dcfd48c/identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2", size = 99145, upload-time = "2025-05-23T20:37:51.495Z" }, ] +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + [[package]] name = "importlib-metadata" version = "8.7.0" @@ -457,6 +576,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "rpds-py", version = "2026.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + [[package]] name = "kiwisolver" version = "1.4.8" @@ -734,6 +881,62 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, ] +[[package]] +name = "odc-geo" +version = "0.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "affine" }, + { name = "cachetools" }, + { name = "numpy" }, + { name = "pyproj" }, + { name = "shapely" }, + { name = "xarray" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/7d/409d01745f55e2577fc8a397fd3c54207be65ef95a99a79ca512c42c473b/odc_geo-0.5.3.tar.gz", hash = "sha256:57aca2e894b8d8e82aa2e2f607630bb33e2b480f68077b52c273ff2b8367dfc7", size = 144174, upload-time = "2026-07-16T22:09:08.962Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/db/cbb4bfa7bb7a1b91f8be7964b2477730d86516756afddaa79cd8683a30d8/odc_geo-0.5.3-py3-none-any.whl", hash = "sha256:5d22c7933a45653d7d0d9b4ead92579bf8bec627d6a3d4bf7e13d77b786e3662", size = 159610, upload-time = "2026-07-16T22:09:07.39Z" }, +] + +[[package]] +name = "odc-loader" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dask", extra = ["array"] }, + { name = "numpy" }, + { name = "odc-geo" }, + { name = "rasterio" }, + { name = "typing-extensions" }, + { name = "xarray" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/01/97e644a936329a31b551b2e480eb40f6f807da1bd1355f45ca3c77014db3/odc_loader-0.6.4.tar.gz", hash = "sha256:bef95deaa4b46c4c73bb359d3a2beca37fb4300f87bc4b12e7b6d646433fd267", size = 48888, upload-time = "2026-01-14T01:21:03.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/8f/22b0a79e458392e377a9784cc93e1bd6aaa3f908d52969a81e0c56e6b59d/odc_loader-0.6.4-py3-none-any.whl", hash = "sha256:3f936841c253535fdb35fb803579e40e5f438d2fa893596004287143817cfee0", size = 58610, upload-time = "2026-01-14T01:21:01.867Z" }, +] + +[[package]] +name = "odc-stac" +version = "0.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "affine" }, + { name = "dask", extra = ["array"] }, + { name = "numpy" }, + { name = "odc-geo" }, + { name = "odc-loader" }, + { name = "pandas" }, + { name = "pystac" }, + { name = "rasterio" }, + { name = "toolz" }, + { name = "typing-extensions" }, + { name = "xarray" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/bb/428165d3ff727791531559d1049fb4fad7899a6f878e769378d0b3269f31/odc_stac-0.5.3.tar.gz", hash = "sha256:00144973748880115f3e261401b0378f14c1bcf6490b0bbeb3c18e9c68508b4e", size = 296231, upload-time = "2026-07-30T04:31:25.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/79/936fc1d69f1a1102f2e0bae76ad2ca1cab3503e69a2ae900e843dbd86a2c/odc_stac-0.5.3-py3-none-any.whl", hash = "sha256:0597532a46003c9462d04e5015ebd9f2e2dec40892c6a4bcc515b278e789179a", size = 44137, upload-time = "2026-07-30T04:31:23.223Z" }, +] + [[package]] name = "packaging" version = "25.0" @@ -881,6 +1084,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/21/2c/5e05f58658cf49b6667762cca03d6e7d85cededde2caf2ab37b81f80e574/pillow-11.2.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:208653868d5c9ecc2b327f9b9ef34e0e42a4cdd172c2988fd81d62d2bc9bc044", size = 2674751, upload-time = "2025-04-12T17:49:59.628Z" }, ] +[[package]] +name = "planetary-computer" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pystac" }, + { name = "pystac-client" }, + { name = "python-dotenv" }, + { name = "pytz" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/d3/ccfb0e823e9beb72b7e6b749f0985c4db1530d4e4a145b1b5d12f5de417e/planetary-computer-1.0.0.tar.gz", hash = "sha256:5958a8e1d8ba1aafc7ac45878df2d7d03405806ae31ed2e675333faebca960cc", size = 17076, upload-time = "2023-07-05T13:06:58.679Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/7a/3965a2b58d172e106c1064af19707d29f86a0142c28ff40185490a0a8cfe/planetary_computer-1.0.0-py3-none-any.whl", hash = "sha256:7af5839f9346c1d23d53fff4e80e955db18a2d81992877816e22dcbc2f90c40d", size = 14163, upload-time = "2023-07-05T13:06:57.06Z" }, +] + [[package]] name = "platformdirs" version = "4.3.8" @@ -977,6 +1199,137 @@ 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.13.4" +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/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, + { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, + { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, + { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, + { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, + { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, + { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + [[package]] name = "pyogrio" version = "0.11.0" @@ -1093,6 +1446,9 @@ dev = [ xarray = [ { name = "affine" }, { name = "dask" }, + { name = "odc-stac" }, + { name = "planetary-computer" }, + { name = "pystac" }, { name = "rioxarray" }, { name = "xarray" }, ] @@ -1105,10 +1461,13 @@ requires-dist = [ { name = "geopandas", specifier = ">=1.1.0" }, { name = "jinja2", specifier = ">=3.1.6" }, { name = "matplotlib", specifier = ">=3.10.1" }, + { name = "odc-stac", marker = "extra == 'xarray'", specifier = ">=0.5.3" }, { name = "pandas", specifier = ">=2.2.3" }, { name = "pillow", specifier = ">=11.1.0" }, + { name = "planetary-computer", marker = "extra == 'xarray'", specifier = ">=1.0.0" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.6.0" }, { name = "pyarrow", specifier = ">=20.0.0" }, + { name = "pystac", marker = "extra == 'xarray'", specifier = ">=1.15.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.4.4" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=6.1.1" }, { name = "pytest-randomly", marker = "extra == 'dev'", specifier = ">=3.16.0" }, @@ -1120,6 +1479,335 @@ requires-dist = [ ] provides-extras = ["xarray", "dev"] +[[package]] +name = "pystac" +version = "1.15.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, + { name = "pystac-ext-classification" }, + { name = "pystac-ext-datacube" }, + { name = "pystac-ext-eo" }, + { name = "pystac-ext-file" }, + { name = "pystac-ext-grid" }, + { name = "pystac-ext-item-assets" }, + { name = "pystac-ext-label" }, + { name = "pystac-ext-mgrs" }, + { name = "pystac-ext-mlm" }, + { name = "pystac-ext-pointcloud" }, + { name = "pystac-ext-projection" }, + { name = "pystac-ext-raster" }, + { name = "pystac-ext-render" }, + { name = "pystac-ext-sar" }, + { name = "pystac-ext-sat" }, + { name = "pystac-ext-scientific" }, + { name = "pystac-ext-storage" }, + { name = "pystac-ext-table" }, + { name = "pystac-ext-timestamps" }, + { name = "pystac-ext-version" }, + { name = "pystac-ext-view" }, + { name = "pystac-ext-xarray-assets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/38/300a9bc5bf30748c7bc768afd8564e0b6958810c8ca0c50f1c0847676a40/pystac-1.15.2.tar.gz", hash = "sha256:4c9a3b2352cad7eb3c226145251c65cbe5c1b04b25d4265b30c015dddf5f391d", size = 7595, upload-time = "2026-07-27T15:47:43.842Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/54/81ca429d8ee0fa1ff78ad3102a612cfb8224aae4ba42ac625c726b13f81c/pystac-1.15.2-py3-none-any.whl", hash = "sha256:b006833253d7e5acd04d877bb7504e14aa1dc98dd0f0ab12f893595030dda2b3", size = 5694, upload-time = "2026-07-27T15:47:42.768Z" }, +] + +[package.optional-dependencies] +validation = [ + { name = "jsonschema" }, +] + +[[package]] +name = "pystac-client" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac", extra = ["validation"] }, + { name = "python-dateutil" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/8d/b98aeffd325fc208e1624cf586d0c4dfb927bc7a2bce20d3b58ee80d2483/pystac_client-0.9.0.tar.gz", hash = "sha256:3908951583bcc6a3aaaf2828024a8e03764e6ca9d9f9f1d8149df587e14dd744", size = 52339, upload-time = "2025-07-18T15:44:41.1Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/d2/5f6367b14c9f250d1a6725d18bd1e9584f5ab1587e292f3a847e59189598/pystac_client-0.9.0-py3-none-any.whl", hash = "sha256:eed146b5980f93646aaa3a59080f11f1dcab6000b0bfbc28b1d0c6fd0a61eda1", size = 41826, upload-time = "2025-07-18T15:44:40.197Z" }, +] + +[[package]] +name = "pystac-core" +version = "1.15.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/67/892aaa463188a259b9538d9d49f420d761ba8fcb8464bf0d8b2cb0a2ef56/pystac_core-1.15.2.tar.gz", hash = "sha256:3720485dd06334e6536afe8f2b8bb0def5c65437170e0251b3417abb89d6b3f3", size = 93857, upload-time = "2026-07-27T15:47:53.04Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/d6/3c65df53ac66532e4451917ed63dcc7c260db30eed0d5c5f1782fcc4128e/pystac_core-1.15.2-py3-none-any.whl", hash = "sha256:96e4acf5eb08ba8be543729b59c3072103bffa7b90283c7497cb25fbff70e6cf", size = 117305, upload-time = "2026-07-27T15:47:51.866Z" }, +] + +[[package]] +name = "pystac-ext-classification" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/ef/4ae97a54de23f84f8ca9637a73efc42dfc343881a9ce159ff40f7dc5c5ce/pystac_ext_classification-2.0.0.tar.gz", hash = "sha256:b744d42eb74fa5038cf00896f3cf4fbb85b79cf208454b7401bd8219e491eb94", size = 21154, upload-time = "2026-06-25T15:49:41.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/d3/f19b3e52938785e3540e5515ba0453291700729d7fa6863c6c07c3faf6bc/pystac_ext_classification-2.0.0-py3-none-any.whl", hash = "sha256:fe8c94fd83b2402cb314f608e835d45d70397614928d6cbb29b12d4728586abd", size = 7036, upload-time = "2026-06-25T15:49:40.585Z" }, +] + +[[package]] +name = "pystac-ext-datacube" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/bc/77eb864cc837ce95c36b67b94f9b102ab6ecc6889a6c131a382f6c5afd08/pystac_ext_datacube-2.2.0.tar.gz", hash = "sha256:a64643cb826c3f2f169c7497b5e5ba218f0a1ababbbd3bad04c57f99df3a67c7", size = 22016, upload-time = "2026-06-25T15:49:43.776Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/12/f988dea7251ba3bb089fa9012ebb459b1229b35da23eedf53a9a0e05faed/pystac_ext_datacube-2.2.0-py3-none-any.whl", hash = "sha256:823538db9751b6e7312a06ec9b6a74f98561830664f45ed43908bea73f624f16", size = 6824, upload-time = "2026-06-25T15:49:42.815Z" }, +] + +[[package]] +name = "pystac-ext-eo" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/f3/b15b8919d6849b4c94ddef1f3dcf12a6c69d3674d9f67e62ced31483f43e/pystac_ext_eo-1.1.0.tar.gz", hash = "sha256:c599b1f4a470eceefb745b26f4dabea05661e843d26c8c59f0d7343b6ac9ab35", size = 21113, upload-time = "2026-06-25T15:49:47.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/39/6bc20044e0513bb290a871aba05608b945f261d294c2cdc78207e98a1938/pystac_ext_eo-1.1.0-py3-none-any.whl", hash = "sha256:ca367465d37bc65d9a4c0cd54c4a04a45deb9904d55ae2c1f37d11734cc38ca0", size = 7366, upload-time = "2026-06-25T15:49:46.629Z" }, +] + +[[package]] +name = "pystac-ext-file" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/37/e8a111a9ba9547bd36a3a2cab3b6797115b57ed518d21b51d7447463ea5c/pystac_ext_file-2.1.0.tar.gz", hash = "sha256:93964df13550d0b016770774a6608728928f3a2c8d92ae03dc2f11bd76dbc6f0", size = 12979, upload-time = "2026-06-25T15:49:53.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/e1/2d5b893323fdc7ca64e58464f6eea91158d0dcfd76a1463c4d9390fcf34b/pystac_ext_file-2.1.0-py3-none-any.whl", hash = "sha256:90b91c82155f763143de2daf1aec37dd0e7f98da7f39f6636a4ede590be9f322", size = 5485, upload-time = "2026-06-25T15:49:52.11Z" }, +] + +[[package]] +name = "pystac-ext-grid" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/0f/a59e46a04e36668e9ae52da4493786f0a0af3513020551e784947ab9efe1/pystac_ext_grid-1.1.0.tar.gz", hash = "sha256:0b8855ff3ddb278bf6f2caf01bb857da50cf23bc322d0722f93b206b7f3673f6", size = 9086, upload-time = "2026-06-25T15:49:48.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/a4/43d317a422528090320166a908f9d110384d67ac2f30353a4a331e990151/pystac_ext_grid-1.1.0-py3-none-any.whl", hash = "sha256:6c8c804a07b4c1fc9dc4058a16d4ec18d7715883a183555c826d312830018db1", size = 3319, upload-time = "2026-06-25T15:49:46.927Z" }, +] + +[[package]] +name = "pystac-ext-item-assets" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/93/fd0d0251ac227b654dabffc15bafad69600c91edf0472d1bba48096cd1c5/pystac_ext_item_assets-1.0.0.tar.gz", hash = "sha256:63b063ae3b0c9508ad84aa06c85513fb2ca09f505ee2aed1a8a1f0c3a906d070", size = 3908, upload-time = "2026-06-25T15:49:56.02Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/63/eca6602e084b1bd764fe0c7e6e5efa351bb6ad5432aaab2a3d56cb1fdbc9/pystac_ext_item_assets-1.0.0-py3-none-any.whl", hash = "sha256:4dd61fcaeb861f1bd0c77299d9e3332865bfded44e061206fc0bb9fd8ce8d156", size = 3418, upload-time = "2026-06-25T15:49:54.905Z" }, +] + +[[package]] +name = "pystac-ext-label" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/e4/b3c010715ec522735f5c5393f685cadf55a687b47c9084fe4ae82bd9c41c/pystac_ext_label-1.0.1.tar.gz", hash = "sha256:34e52bbc79d46a5a0f76833ce0f656de65b425a41ca362e705be68430196abc7", size = 8339, upload-time = "2026-06-25T15:49:51.709Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/f5/583b8f20f8c041fb3decb36ab4b5be703f64cc84883ef152373f9a075a99/pystac_ext_label-1.0.1-py3-none-any.whl", hash = "sha256:9b089b290ce953916cd087a86d108262cf74ca39d0f00e27af6eeaf1ab2be167", size = 7871, upload-time = "2026-06-25T15:49:50.341Z" }, +] + +[[package]] +name = "pystac-ext-mgrs" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/d6/5f54dccb399f0246e12d5b2a9c1ca5269f1adcf993a3812ecf897663e004/pystac_ext_mgrs-1.0.0.tar.gz", hash = "sha256:31d79b288f2523640138aff1b12acd2b67d7a40b1822909a04700fb048861d11", size = 6610, upload-time = "2026-06-25T15:49:54.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/c2/aa4c889ffbd5d95b9cbcb78163399feb51eae3968f387f87f26efef2bf59/pystac_ext_mgrs-1.0.0-py3-none-any.whl", hash = "sha256:4efe67b4dac4fb7c3057985a056e59275371c535d84c82d0a55f110f9144f9f0", size = 3849, upload-time = "2026-06-25T15:49:53.206Z" }, +] + +[[package]] +name = "pystac-ext-mlm" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/5c/7b540355fc4be970f076047a222216f2af513870ebfca53c48242de5e078/pystac_ext_mlm-1.4.0.tar.gz", hash = "sha256:fdaf464e729071348b3921e908dbe3c2e1cce5d65153ee19995e2e7315e9edf9", size = 35919, upload-time = "2026-06-25T15:49:53.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/65/a4f7b03201993bd647a3260d9b1663b1d0d28145d35bc69562334ef49a28/pystac_ext_mlm-1.4.0-py3-none-any.whl", hash = "sha256:69dac825477b7dd458ad5392aa72188377b267f267b6b28a60e883daed0ef252", size = 13599, upload-time = "2026-06-25T15:49:52.664Z" }, +] + +[[package]] +name = "pystac-ext-pointcloud" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/51/cf14464aad24d1547dd0e5c06bcfba9627a0b0510d8ec147e17a7c90c2de/pystac_ext_pointcloud-1.0.0.tar.gz", hash = "sha256:92a4bc88fe6eb15ad27c75358b1dbf5bf40ded26df4cd6b8bfc16a4c9a838764", size = 12581, upload-time = "2026-06-25T15:49:59.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/e9/783c234f711661603c21c5c83c6cbb41efcb2cd0ca791cf27316498088b2/pystac_ext_pointcloud-1.0.0-py3-none-any.whl", hash = "sha256:3bc688b3da93dc0dbf91b357026eb7648ec3e49a3008003859648b80dfb55732", size = 6166, upload-time = "2026-06-25T15:49:58.81Z" }, +] + +[[package]] +name = "pystac-ext-projection" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/55/2d2230a5bb55d9e954338e71c5fd469ff63097532f2b45fa219f607c075a/pystac_ext_projection-2.0.0.tar.gz", hash = "sha256:72517587636c3d0f325cf11dfa75e0431570c6a13305bf53d0f95b2548deec1f", size = 72074, upload-time = "2026-06-25T15:50:02.03Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/27/cede1d2f621f4c399ee2434072bf5a8bba0e3b0e3369547664f343e0ed84/pystac_ext_projection-2.0.0-py3-none-any.whl", hash = "sha256:892e9852a1a81cf1be1b79e3ad52e9b11f780143d3a75d67cde3e4fd31ec83e4", size = 7114, upload-time = "2026-06-25T15:50:00.785Z" }, +] + +[[package]] +name = "pystac-ext-raster" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/04/2459cd1a4b954fc3320594ef86e6f4379f15d0cf4a63259852fb4b4a9b5b/pystac_ext_raster-1.1.0.tar.gz", hash = "sha256:19dd7c993defe450fa90b3fd690f30a6191b701fc704f1626748bdb57d9da1ed", size = 30679, upload-time = "2026-06-25T15:50:05.636Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/1e/9bced0383702b1ceb240a4aa4c32f0f4f76f6666325a7d35c3e499ef1104/pystac_ext_raster-1.1.0-py3-none-any.whl", hash = "sha256:07c312ae956d70ccdb1da7af67fc92793c2546ca8e26ca384013ebf5c674b410", size = 6567, upload-time = "2026-06-25T15:50:04.448Z" }, +] + +[[package]] +name = "pystac-ext-render" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/ba/e8711c8b46719f4d8e74fd73c5ae66fa5cd0b79e0dc7770d00bc1d14adcf/pystac_ext_render-2.0.0.tar.gz", hash = "sha256:ed53926d0af556c98d5f6bbfd86b8d02d635df24b70607f37f386cea7713b4f7", size = 9611, upload-time = "2026-06-25T15:50:00.903Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/63/3162c536d3f3cacf627b7a74915dc90abb10cf3baa5ed9b28b3b92b5914c/pystac_ext_render-2.0.0-py3-none-any.whl", hash = "sha256:69c3a8307d9528b8ae5baadde76dae9e546c19c1f4210a57daf235f8e9d9e427", size = 5020, upload-time = "2026-06-25T15:49:59.974Z" }, +] + +[[package]] +name = "pystac-ext-sar" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c8/30/e229a96be9e03157c92eb7f3bfafbd84702372bd1a7ed23e734094c48ead/pystac_ext_sar-1.0.0.tar.gz", hash = "sha256:0d49130fc55152f039f22ac843df35e1c5b2a1a667a4ee91d4a9c5b3ecd379ca", size = 11122, upload-time = "2026-06-25T15:50:01.836Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/75/8d62c8d697740c01c01d707446b53b332a466d6caf174187588000daa9a8/pystac_ext_sar-1.0.0-py3-none-any.whl", hash = "sha256:4b73d97e4c1cdba1c1d6cc1d69181fed31eefb0ec164500b636d6a8d8bca271c", size = 6262, upload-time = "2026-06-25T15:50:00.762Z" }, +] + +[[package]] +name = "pystac-ext-sat" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/d7/0492bae1d417dc4990748183a950fd1e6acbc202a2b11bbb73cc75a37f34/pystac_ext_sat-1.0.0.tar.gz", hash = "sha256:2163b31d3044f9c2965abd9cb4144482813068ff9f99107a240eed06cf78bace", size = 9875, upload-time = "2026-06-25T15:50:12.336Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/ca/20d80d50b5108f649383568d3c103e9cc0cf12e53f283562f9dfc1774dd3/pystac_ext_sat-1.0.0-py3-none-any.whl", hash = "sha256:ee951eee24b47c576863bbf5890957b7094a6a2bb0ef8fc967a4d327dff571fd", size = 4576, upload-time = "2026-06-25T15:50:11.303Z" }, +] + +[[package]] +name = "pystac-ext-scientific" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/f6/1bf1ec97be09266a258f53620020573d8321956e390361daa81d0905b9ae/pystac_ext_scientific-1.0.0.tar.gz", hash = "sha256:b55128b7b2281c28dcfc3c00c008fe4391c84b47a7572c4770766f1e96eec5b5", size = 12764, upload-time = "2026-06-25T15:50:06.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/f7/cde7d45905f4f15449f142c7d6f81de488b3920bf3fb063b2382d1ffefba/pystac_ext_scientific-1.0.0-py3-none-any.whl", hash = "sha256:1f78efcd34a4b4344c70d80ddcde70785815e738455e3c1d35b5e843ca671207", size = 5174, upload-time = "2026-06-25T15:50:05.179Z" }, +] + +[[package]] +name = "pystac-ext-storage" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/dc/57f0dfa715f6bdc0de1830359969ffb019424838c875f899c011fdbf2af0/pystac_ext_storage-2.0.0.tar.gz", hash = "sha256:58b28988e49a188cc8ee09e8bc88440aec9985f0d3ccd295b6209215896e4250", size = 13063, upload-time = "2026-06-25T15:50:08.61Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/af/5aaa33cf912da18e178c7bc15633c3c9f1cc9a22efcad7f78964136e4e14/pystac_ext_storage-2.0.0-py3-none-any.whl", hash = "sha256:8f1d809a8f5efebe051450fcfce3ae13ef4c7ad9133b43b2a1f4df69e74645c2", size = 7353, upload-time = "2026-06-25T15:50:07.687Z" }, +] + +[[package]] +name = "pystac-ext-table" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/06/d4ad9edaf4bae2922cf86a6ac216375d530fa70e4f13bff28b52c595a2d1/pystac_ext_table-1.2.0.tar.gz", hash = "sha256:c7035db662272a84f1149e7d694c8409f85093cc056cdc655c06e9b9f5cefe37", size = 8003, upload-time = "2026-06-25T15:50:09.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/52/6209f65f1b4f966525486b7eb39f82f1cd0f8aae592e52c679b520e969ea/pystac_ext_table-1.2.0-py3-none-any.whl", hash = "sha256:461e8475eaee81190c36db818150e0b24d14d46b021f063a928ff6a0ee77a971", size = 4430, upload-time = "2026-06-25T15:50:08.808Z" }, +] + +[[package]] +name = "pystac-ext-timestamps" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/74/97061e036f74c850b63450ab425f7c1aa2681582ce53b98ed4f7235acc7b/pystac_ext_timestamps-1.1.0.tar.gz", hash = "sha256:d78250e3ddc61b825348444ad05c8cff8c2aa1cd3d18155b880783ee880520d3", size = 8544, upload-time = "2026-06-25T15:50:18.662Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/22/d0e9f1e46da156c34e070ba04ebca1bba67b4b60724118cfb98c0b83b368/pystac_ext_timestamps-1.1.0-py3-none-any.whl", hash = "sha256:17990ad4827bfd0798010d247fa2f3d770f78d80180579cbe63d8267fb3e8d39", size = 4252, upload-time = "2026-06-25T15:50:17.677Z" }, +] + +[[package]] +name = "pystac-ext-version" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/48/0d67ffdc3c930cd64805e15603372f2a759d2ad453f31d979fefda637999/pystac_ext_version-1.2.0.tar.gz", hash = "sha256:aac23024e76cd64c9bec46a8b49f884f2e8c89cc3d0fe317f5f58dfaa56d1aa0", size = 12543, upload-time = "2026-06-25T15:50:19.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/bb/c0a732f1827233e91200b4c14538e8cc4dafa9e843c46c048462212e2b72/pystac_ext_version-1.2.0-py3-none-any.whl", hash = "sha256:95ba628abe934098356654f2bd57423475d1cb59e2f49c44dd7e2ef5392efefd", size = 5366, upload-time = "2026-06-25T15:50:18.547Z" }, +] + +[[package]] +name = "pystac-ext-view" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/1d/663184b0c94583ed2c3d2fbe1f1b3ab8da27ee7ff5695da2d0d42652e610/pystac_ext_view-1.0.0.tar.gz", hash = "sha256:cb677dcd49f9a62824087d150f01eea97fc24c577971edb60374c9e48e517311", size = 9698, upload-time = "2026-06-25T15:50:11.379Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/1f/aab5f54ee091b47b6edf22dea97863c36996ca7886b32e8772374fcbb5e9/pystac_ext_view-1.0.0-py3-none-any.whl", hash = "sha256:3000615569d1826093a6b231778e7ddb1afb106dd516e8029a0083bfcd166d7d", size = 4479, upload-time = "2026-06-25T15:50:10.359Z" }, +] + +[[package]] +name = "pystac-ext-xarray-assets" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/a0/1bcb9ba80eb0e0a6cc4605520d831d7118863559f13f797f271430bad125/pystac_ext_xarray_assets-1.0.0.tar.gz", hash = "sha256:da4f4877c018d531b6b5c45f20bc64842f14587ae86bac8d2acf110804c773d1", size = 6703, upload-time = "2026-06-25T15:50:18.944Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/cc/4f310e5acb0c59c8b3db417fc903a055dc01ac7e7f7e82c941304f30b4d9/pystac_ext_xarray_assets-1.0.0-py3-none-any.whl", hash = "sha256:a31e90e7e9bc5ae191e7449df28c39857c818e8c1cf2c09a2eceaa7c6e13241c", size = 3614, upload-time = "2026-06-25T15:50:17.871Z" }, +] + [[package]] name = "pytest" version = "8.3.5" @@ -1174,6 +1862,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + [[package]] name = "pytz" version = "2025.2" @@ -1261,6 +1958,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/88/9db5f49ebfdd9c12365e4cac76c34ccb1a642b1c8cbab4124b3c681495de/rasterio-1.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:1839960e2f3057a6daa323ccf67b330f8f2f0dbd4a50cc7031e88e649301c5c0", size = 25424949, upload-time = "2024-12-02T14:49:11.742Z" }, ] +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "rpds-py", version = "2026.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + [[package]] name = "rioxarray" version = "0.19.0" @@ -1277,6 +2004,258 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/2f/63d2cacc0e525f8e3398bcf32bd3620385f22cd1600834ec49d7f3597a7b/rioxarray-0.19.0-py3-none-any.whl", hash = "sha256:494ee4fff1781072d55ee5276f5d07b63d93b05093cb33b926a12186ba5bb8ef", size = 62151, upload-time = "2025-04-21T17:46:52.801Z" }, ] +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "rpds-py" +version = "2026.6.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/1f/a2dca5ffdbf1d475ffc4e80e4d5d720ff3a00f691795910116960ee12511/rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7", size = 342174, upload-time = "2026-06-30T07:14:54.821Z" }, + { url = "https://files.pythonhosted.org/packages/4d/dc/323d08583c0832911768663d1944f0107fcd4088704858d84b5e06d105a0/rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911", size = 345513, upload-time = "2026-06-30T07:14:56.515Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2a/e31989834d18d2f26ec1d2774c5b1eb3331df4ea8ada525175294c94b48a/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4", size = 373783, upload-time = "2026-06-30T07:14:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/87/fe/e80107ee3639585c9941c17d6a42cd65325022f656c023191fce78c324c8/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261", size = 378316, upload-time = "2026-06-30T07:14:59.077Z" }, + { url = "https://files.pythonhosted.org/packages/22/6f/81e3adf81acfb6fa694de2a6e4e7d8863121e3e0799e0a7725e6cf5679c4/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278", size = 499423, upload-time = "2026-06-30T07:15:00.488Z" }, + { url = "https://files.pythonhosted.org/packages/2d/9a/41263969df0ce3d9af2a96d5005a288200af1989aed3354bfceb5fc0b21f/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9", size = 386077, upload-time = "2026-06-30T07:15:01.911Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/7e98f468bd50346faff5b10e5297374b443bfdddacc8e9fbc65984539597/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7", size = 371315, upload-time = "2026-06-30T07:15:03.317Z" }, + { url = "https://files.pythonhosted.org/packages/99/3c/2b973b4d371906a134b03decfea7f5d9835a2c6d263454392e15b64b5b18/rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3", size = 383502, upload-time = "2026-06-30T07:15:04.627Z" }, + { url = "https://files.pythonhosted.org/packages/98/2a/12e2799500af0a307bca76b63361c51f9fe479223561489c29eea1f2ee41/rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da", size = 402673, upload-time = "2026-06-30T07:15:05.856Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e3/21e5872d165fe08be4f229e3d5ee9d90019c0bf0e5538de60dbd54009450/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4", size = 549964, upload-time = "2026-06-30T07:15:07.159Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d0/5ee0fe36844297de8123bee27bc12078c1a7416ad9f1b8a8ca18d6b0c0ac/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6", size = 615446, upload-time = "2026-06-30T07:15:08.531Z" }, + { url = "https://files.pythonhosted.org/packages/b1/80/1ea5873cb683f2fbe5f21b23ea1f6d179ead19f3c5b249b7eb5dca568ef2/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93", size = 576975, upload-time = "2026-06-30T07:15:09.97Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e1/90ef639217a5ddb15b7f4f61b1c33911fd044ad03c311bafdd2bcab85582/rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a", size = 204453, upload-time = "2026-06-30T07:15:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b7/b7a1695d7af36f521fb11e80d6d3adbd744f73b921859bd3c2a2c0dc706f/rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127", size = 223219, upload-time = "2026-06-30T07:15:12.476Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a2/145afacf796e4506062825941176ad9445c2dcf2b3b6a1f13d3030a15e19/rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804", size = 219137, upload-time = "2026-06-30T07:15:13.631Z" }, + { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" }, + { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" }, + { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" }, + { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" }, + { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" }, + { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" }, + { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" }, + { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" }, + { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" }, + { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" }, + { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" }, + { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" }, + { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" }, + { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" }, + { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" }, + { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" }, + { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" }, + { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" }, + { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" }, + { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" }, + { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" }, + { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" }, + { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" }, + { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" }, + { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" }, + { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" }, + { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" }, + { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" }, + { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" }, + { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" }, + { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" }, + { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" }, + { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" }, + { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" }, + { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" }, + { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" }, + { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" }, + { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" }, + { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" }, + { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" }, + { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" }, + { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" }, + { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9c/f0d19ac587fd0e4ab6b72cda355e9c5a6166b01ef7e064e437aef8eb9fef/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f", size = 349791, upload-time = "2026-06-30T07:17:33.315Z" }, + { url = "https://files.pythonhosted.org/packages/38/c7/1d49d204c9fd2ee6c537601dc4c1ba921e03363ca576bfab94a00254ac9a/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171", size = 352842, upload-time = "2026-06-30T07:17:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e5/c0b5dc93cd0d4c06ce1f438907649514e2ea077bcd911e3154a51e96c38e/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90", size = 382094, upload-time = "2026-06-30T07:17:36.514Z" }, + { url = "https://files.pythonhosted.org/packages/0d/54/ec0e907b4ca8d541112db352409bd15f871c9b243e0c92c9b5a46ae96f01/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca", size = 388662, upload-time = "2026-06-30T07:17:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f4/921c22a4fd0f1c1ac13a3996ffbf0aa67951e2c8ad0d1d9574938a2932e8/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9", size = 504896, upload-time = "2026-06-30T07:17:39.689Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1b/a114b972cefa1ab1cdb3c7bb177cd3844a12826c507c722d3a73516dbbaf/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c", size = 391545, upload-time = "2026-06-30T07:17:41.336Z" }, + { url = "https://files.pythonhosted.org/packages/4e/98/af9b3db77d47fcbe6c8c1f36e2c2147ec70292819e99c325f871584a1c11/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9", size = 380059, upload-time = "2026-06-30T07:17:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ba/0efd8668b97c1d26a61566386c636a7a7a09829e474fdf807caa15a2c844/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41", size = 393235, upload-time = "2026-06-30T07:17:44.637Z" }, + { url = "https://files.pythonhosted.org/packages/62/90/8c139ee9690f73b0829f32647de6f40d826f8f443af6fa72644f96351aac/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c", size = 413008, upload-time = "2026-06-30T07:17:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/0043896fdd7828ce09a1d9a8b06433714d0960fc4ff3fc4aa72b666b764e/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9", size = 558118, upload-time = "2026-06-30T07:17:47.759Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/02355f0e134f783a8f9814c4680a1bd311d37671577a5964ea838573ff37/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76", size = 623138, upload-time = "2026-06-30T07:17:49.355Z" }, + { url = "https://files.pythonhosted.org/packages/10/85/48f0abdcef5cce4e034c7a5b0ceeceba0b01bf0d942824f4bb720afe2dec/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826", size = 586486, upload-time = "2026-06-30T07:17:51.141Z" }, +] + [[package]] name = "rpy2" version = "3.6.0" @@ -1461,11 +2440,23 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.13.2" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +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/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, + { 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]] @@ -1489,6 +2480,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" }, ] +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + [[package]] name = "virtualenv" version = "20.31.2"