diff --git a/Dockerfile b/Dockerfile
index d817003..52bdb93 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -9,7 +9,7 @@ RUN apt-get update && apt-get install -y build-essential libsqlite3-dev zlib1g-d
# ==========================================
# STAGE 2: Python Environment Builder (The "Fat" Stage)
# ==========================================
-FROM ghcr.io/osgeo/gdal:ubuntu-small-3.10.0 AS python-builder
+FROM ghcr.io/osgeo/gdal:ubuntu-full-3.10.0 AS python-builder
ENV UV_COMPILE_BYTECODE=1
ENV UV_LINK_MODE=copy
@@ -46,7 +46,7 @@ RUN uv pip install --no-cache .
# ==========================================
# STAGE 3: Production Runtime (The "Skinny" Stage)
# ==========================================
-FROM ghcr.io/osgeo/gdal:ubuntu-small-3.10.0 AS prod
+FROM ghcr.io/osgeo/gdal:ubuntu-full-3.10.0 AS prod
ENV DEBIAN_FRONTEND=noninteractive
ENV PLAYWRIGHT_BROWSERS_PATH=0
diff --git a/pyproject.toml b/pyproject.toml
index 6a358ce..8ca05f1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -39,8 +39,6 @@ dependencies = [
"geopandas",
"azure-identity",
"rich",
- #"morecantile",
- #"mapbox_vector_tile",
"sympy",
"pydantic",
"pmtiles",
@@ -62,6 +60,11 @@ dependencies = [
"h5py>=3.16.0",
"fsspec>=2026.4.0",
"satpy>=0.59.0",
+ "matplotlib>=3.10.9",
+ "netcdf4>=1.7.3",
+ "h5netcdf>=1.8.1",
+ "spacetrack>=1.4.0"
+
]
[project.optional-dependencies]
diff --git a/rapida/admin/util.py b/rapida/admin/util.py
index c4e8ffa..2792c2b 100644
--- a/rapida/admin/util.py
+++ b/rapida/admin/util.py
@@ -13,7 +13,7 @@ def is_int(val):
else:
return False
-def bbox_to_geojson_polygon(west, south, east, north):
+def bbox_to_geojson_polygon(west, south, east, north, as_string=False):
"""
Converts a bounding box to a GeoJSON Polygon geometry.
@@ -44,7 +44,9 @@ def bbox_to_geojson_polygon(west, south, east, north):
"coordinates": coordinates
}
}
-
+ if as_string:
+ import json
+ return json.dumps(geojson, indent=2)
return geojson
diff --git a/rapida/cli/assess.py b/rapida/cli/assess.py
index 5e6c685..60b14d2 100644
--- a/rapida/cli/assess.py
+++ b/rapida/cli/assess.py
@@ -167,6 +167,10 @@ def build_variable_help():
show_default=True,help=f'The year for which to compute population' )
@click.option('--datetime-range', '-dt', required=False, type=str, callback=validate_datetime_range, default=datetime.date.today().strftime('%Y-%m-%d'),
help=f"Optional. Date range for landuse component in YYYY-MM-DD/YYYY-MM-DD format or single date YYYY-MM-DD (12 months range). Only valid when 'landuse' component is selected. Start date must be after end date, end date must be before today, and at least 1 day apart.")
+
+@click.option("--outage-date", "outage_date", type=click.DateTime(formats=["%Y-%m-%d"]), required=False,
+ help='The human experience of a specific night, local time zone matched to the center of bbox')
+
@click.option('--cloud-cover', '-cc', required=False, type=int, multiple=False, default=5,
show_default=True,help=f"Optional. Minimum cloud cover rate to search items for landuse component.")
@click.option('-p', '--project',
@@ -181,7 +185,7 @@ def build_variable_help():
help="Set log level to debug"
)
@click.pass_context
-def assess(ctx, all=False, components=None, variables=None, year=None, datetime_range=None, cloud_cover=None, project: str = None, force=False, debug=False):
+def assess(ctx, all=False, components=None, variables=None, year=None, datetime_range=None, outage_date=None, cloud_cover=None, project: str = None, force=False, debug=False):
"""
Assess/evaluate a specific geospatial exposure components/variables
@@ -265,6 +269,7 @@ def assess(ctx, all=False, components=None, variables=None, year=None, datetime
variables=variables,
target_year=year,
datetime_range=datetime_range,
+ outage_date=outage_date,
cloud_cover=cloud_cover,
force=force)
diff --git a/rapida/cli/init.py b/rapida/cli/init.py
index aa9f10e..3ff4f36 100644
--- a/rapida/cli/init.py
+++ b/rapida/cli/init.py
@@ -10,6 +10,7 @@
from rapida.components.deprivation.variables import generate_variables as gen_depriv_vars
from rapida.components.landuse.variables import generate_variables as gen_landuse_vars
from rapida.components.gdp.variables import generate_variables as gen_gdp_vars
+from rapida.components.ntl.variables import generate_variables as gen_ntl_vars
from rapida.util.setup_logger import setup_logger
logger = logging.getLogger(__name__)
@@ -62,6 +63,7 @@ def setup_prompt(session: Session):
"elegrid": gen_electric_vars(),
"landuse": gen_landuse_vars(),
"gdp": gen_gdp_vars(),
+ "ntl": gen_ntl_vars()
}
}
session.config.update(vars_dict)
diff --git a/rapida/cli/ntl.py b/rapida/cli/ntl.py
index 661bfa4..0652c17 100644
--- a/rapida/cli/ntl.py
+++ b/rapida/cli/ntl.py
@@ -5,18 +5,19 @@
from typing import Iterable
import click
import tempfile
+
+
from rapida.cli import RapidaCommandGroup
-from rapida.ntl.nasa.const import ARCHIVE, OPERATIONAL, PROCESSING_LEVEL_NAMES, PRODUCT_NAMES, PRODUCTS, \
- NTL_FILENAME_PATTERN, ROUTES, COLLECTIONS
+from rapida.ntl.nasa.const import ARCHIVE, OPERATIONAL, PROCESSING_LEVEL_NAMES, PRODUCT_NAMES, NTL_FILENAME_PATTERN, ROUTES, COLLECTIONS
from rapida.ntl.nasa.search import search as nasa_search
from rapida.ntl.noaa.search import async_search_granules, VIIRSNavigator
from rapida.util.bbox_param_type import BboxParamType
-from rapida.ntl.nasa.io import download as download_from_nasa, bulk_download
+from rapida.ntl.nasa.io import download as download_from_nasa, bulk_download as bdownload
from rapida.ntl.noaa.const import SOURCE_NAMES, PRODUCT_NAMES as OPER_PRODUCT_NAMES
from rapida.ntl.noaa.io import download as download_from_noaa, bytesto
from rich.table import Table
-from rapida.ntl.nasa.io import bulk_download as bdownload
-
+from rapida.ntl.fetch import DELIVERABLES, fetch as fetch_ntl
+from rapida.ntl.outage import detect_outage
logger = logging.getLogger(__name__)
@@ -41,27 +42,7 @@ def handle_parse_result(self, ctx, opts, args):
return super().handle_parse_result(ctx, opts, args)
-def validate_products_strict(ctx, param, value):
- if not value:
- return value
-
- # Check for mixed catalogs
- has_nrt = any('nrt' in p.lower() for p in value)
- has_std = any('nrt' not in p.lower() for p in value)
- nrt_choices = [item for p in COLLECTIONS['LANCEMODIS'].values() for item in p]
- std_choices = [item for p in COLLECTIONS['LAADS'].values() for item in p]
- if has_nrt and has_std:
- raise click.BadParameter(
- f"Cannot mix NRT and Standard products in the same command. "
- f"They belong to different catalogs:\n"
- f"LANCEMODIS - NOAA operational: {', '.join(nrt_choices)}\n"
- f"LAADS - NASA archive : {', '.join(std_choices)}"
- )
-
- if has_nrt:
- return tuple(nrt_choices)
- else:
- return tuple(std_choices)
+
class NASAProductsChoiceOption(click.Option):
"""
@@ -101,7 +82,7 @@ def search():
type=BboxParamType(),
help='Bounding box xmin/west, ymin/south, xmax/east, ymax/north'
)
-@click.option("--date", "target_date",
+@click.option("--date", "nominal_date",
type=click.DateTime(formats=["%Y-%m-%d"]),
required=True,
help=''
@@ -132,10 +113,10 @@ def search():
@click.pass_context
-async def search_noaa(ctx, bbox:tuple[numbers.Number]=None, target_date:datetime=None, satellites:list[str] = [], cmask:bool=None ):
+async def search_noaa(ctx, bbox:tuple[numbers.Number]=None, nominal_date:datetime=None, satellites:list[str] = [], cmask:bool=None):
progress = ctx.obj.get('progress')
- table = Table(title=f"VIIRS satellites granules for the night of {target_date.date()} covering {bbox}",
+ table = Table(title=f"VIIRS satellites granules for the night of {nominal_date.date()} covering {bbox}",
title_style="bold yellow")
table.add_column("Position", justify="center", style="white")
table.add_column("Satellite", style="green", justify='center')
@@ -149,7 +130,7 @@ async def search_noaa(ctx, bbox:tuple[numbers.Number]=None, target_date:datetime
table.add_column("BBOX intersection (%)", justify="center", style="white")
granules = await async_search_granules(
- satellites=satellites, target_date=target_date, bbox=bbox,
+ satellites=satellites, nominal_date=nominal_date, bbox=bbox,
cmask=cmask, progress=progress)
if granules:
for i, granule in enumerate(granules, start=1):
@@ -205,7 +186,7 @@ async def search_noaa(ctx, bbox:tuple[numbers.Number]=None, target_date:datetime
)
@click.pass_context
-def search_nasa(ctx, bbox:tuple[numbers.Number]=None, nominal_date:datetime=None, stream:str = None, processing_level:str=None, route:str=None):
+def search_nasa(ctx, bbox:tuple[numbers.Number, numbers.Number, numbers.Number, numbers.Number]=None, nominal_date:datetime=None, stream:str = None, processing_level:str=None, route:str=None):
progress = ctx.obj.get('progress')
@@ -252,6 +233,12 @@ def download():
help='A specific tile number conforming to NASA BalckMarble 10x10 degrres tile numbering. Ex: h21v03 '
)
+@click.option('-b', '--bbox',
+ required=False,
+ type=BboxParamType(),
+ help='Bounding box xmin/west, ymin/south, xmax/east, ymax/north'
+ )
+
@click.option(
"--dst-dir",
"dst_dir", # Function argument name
@@ -268,10 +255,16 @@ def download():
@click.pass_context
-async def download_nasa(ctx, timestamp:str = None, product:str=None, tile:str=None, dst_dir:str=None):
+async def download_nasa(ctx, timestamp:str = None, product:str=None, tile:str=None,
+ bbox:tuple[float, float, float, float]=None,dst_dir:str=None):
progress = ctx.obj.get('progress')
+ if tile and bbox:
+ raise click.UsageError("Illegal usage: `--tile` and `--bbox` are mutually exclusive.")
+
+ downloaded_files = await download_from_nasa(timestamp=timestamp, product=product,
+ tile=tile, bbox=bbox, dst_dir=dst_dir,progress=progress)
+
- downloaded_files = await download_from_nasa(timestamp=timestamp, product=product, tile=tile, dst_dir=dst_dir,progress=progress)
if downloaded_files:
table = Table(title=f"Downloaded files for {product.upper()} {timestamp} ", title_style="bold yellow")
@@ -280,13 +273,14 @@ async def download_nasa(ctx, timestamp:str = None, product:str=None, tile:str=No
table.add_column("Timestamp", style="red", justify='center')
table.add_column("Tile", style="red", justify='center')
table.add_column("Size", style="green", justify='center')
- for local_file_path in downloaded_files:
- _, file_name = os.path.split(local_file_path)
- file_size = os.path.getsize(local_file_path)
- m = NTL_FILENAME_PATTERN.match(file_name)
- meta = m.groupdict()
- tile = meta['tile']
- table.add_row(local_file_path, timestamp, tile, f'{file_size}')
+ for timestamp, files in downloaded_files.items():
+ for local_file_path in files:
+ _, file_name = os.path.split(local_file_path)
+ file_size = os.path.getsize(local_file_path)
+ m = NTL_FILENAME_PATTERN.match(file_name)
+ meta = m.groupdict()
+ tile = meta['tile']
+ table.add_row(local_file_path, timestamp, tile, f'{file_size}')
progress.console.print(table)
@@ -301,9 +295,8 @@ async def download_nasa(ctx, timestamp:str = None, product:str=None, tile:str=No
)
@click.option("--timestamp", "-t", "timestamp", type=str, required=True, help='Granule timestamp string as date and time. Ex: 202604152232 ')
-@click.option(
- "--products",
- "-p",
+@click.option("-p",
+ "--products",
"products",
type=click.Choice(OPER_PRODUCT_NAMES, case_sensitive=False),
default=OPER_PRODUCT_NAMES,
@@ -424,15 +417,126 @@ async def bulk_download(ctx, bbox:tuple[numbers.Number]=None, start_date:datetim
)
+@ntl.command(short_help=f'Find and download best NTL data for a specific event associated with an area and date ')
+
+@click.option('-b', '--bbox',
+ required=True,
+ type=BboxParamType(),
+ help='Bounding box xmin/west, ymin/south, xmax/east, ymax/north'
+ )
+
+
+@click.option("--date", "nominal_date",
+ type=click.DateTime(formats=["%Y-%m-%d"]),
+ required=True,
+ help='The human experience of a specific night, local time zone matched to the center of bbox'
+ )
+
+
+@click.option("-d","deliverable",
+ type=click.Choice(DELIVERABLES, case_sensitive=False),
+ required=True,
+ help=f'One or more of the RAPIDA NTL deliverables.'
+ )
+
+@click.option(
+ "--dst-dir",
+ "dst_dir", # Function argument name
+ type=click.Path(
+ exists=False, # Set to True if you want Click to fail if the dir doesn't exist yet
+ file_okay=False, # Strictly enforce that this is a directory, not a file
+ dir_okay=True,
+ resolve_path=True # Resolves relative paths (like '.') to absolute paths automatically
+ ),
+ default=tempfile.gettempdir(), # Defaults to the current working directory
+ show_default=True, # Tells the user what the default is in the --help menu
+ help="Destination directory to save the downloaded the images."
+)
+
+
+@click.pass_context
+async def fetch(ctx, bbox:tuple[numbers.Number]=None, nominal_date:datetime=None, deliverable:str=None, dst_dir:str=None):
+
+ progress = ctx.obj.get('progress')
+ return await fetch_ntl(bbox=bbox,nominal_date=nominal_date, deliverable=deliverable, progress=progress, dst_dir=dst_dir )
+
+
+@ntl.command(short_help=f'Execute crisis impact detection (48h Alerts / 72h Assessments)')
+
+@click.option('-b', '--bbox',
+ required=True,
+ type=BboxParamType(),
+ help='Bounding box xmin/west, ymin/south, xmax/east, ymax/north'
+ )
+
+
+@click.option("--date", "nominal_date",
+ type=click.DateTime(formats=["%Y-%m-%d"]),
+ required=True,
+ help='The human experience of a specific night, local time zone matched to the center of bbox'
+ )
+
+@click.option(
+ "--dst-dir",
+ "dst_dir", # Function argument name
+ type=click.Path(
+ exists=False, # Set to True if you want Click to fail if the dir doesn't exist yet
+ file_okay=False, # Strictly enforce that this is a directory, not a file
+ dir_okay=True,
+ resolve_path=True # Resolves relative paths (like '.') to absolute paths automatically
+ ),
+ default=tempfile.gettempdir(), # Defaults to the current working directory
+ show_default=True, # Tells the user what the default is in the --help menu
+ help="Destination directory to save the downloaded the images."
+)
+
+@click.option("-d","deliverable",
+ type=click.Choice(DELIVERABLES, case_sensitive=False),
+ required=True,
+ help=f'One or more of the RAPIDA NTL deliverables.'
+ )
+
+
+@click.option('-ot', "--percentage_drop",
+ type=int,
+ default=50,
+ required=False,
+ help="Specify the outage drop threshold that wil determine the spatial structure of an outage event, "
+)
+
+@click.option(
+ '--cmask', '-cm', "mask_clouds",
+ is_flag=True,
+ help=(
+ "Enable strict Cloud Masking (ignores pixels with NASA quality flags of 3). "
+ "Disable this flag during major storm events to prevent atmospheric noise "
+ "from erroneously masking out legitimate blackout signals."
+ ),
+ default=False
+)
+@click.option(
+ '--display', "display",
+ is_flag=True,
+ help=(
+ "Show a graphic visualization of the outage analysis."
+ "Useful to inspect the input imagery and debug/understand the outage results "
+ ),
+ default=False
+)
+
+@click.pass_context
+async def detect(ctx, bbox:tuple[numbers.Number]=None, nominal_date:datetime=None, deliverable:str=None,
+ mask_clouds:bool=True, dst_dir:str=None, percentage_drop:int=None, display:bool=False):
+ progress = ctx.obj.get('progress')
+ return await detect_outage(
+ bbox=bbox, nominal_date=nominal_date, deliverable=deliverable, dst_dir=dst_dir,
+ progress=progress, mask_clouds=mask_clouds, percentage_drop=percentage_drop, display=display
+ )
+
+
-# @ntl.command(short_help=f'Execute crisis impact detection (48h Alerts / 72h Assessments)')
-# @click.pass_context
-# async def detect(ctx):
-# logger.info('Detecting impact on the ground')
-#
-#
# @ntl.command(short_help=f'Track long-term resilience and recovery curves (2-3 Week horizon)')
# async def monitor():
# logger.info('Monitoring recovery')
diff --git a/rapida/components/landuse/search_utils/mgrsconv.py b/rapida/components/landuse/search_utils/mgrsconv.py
index 9b4738b..1024293 100644
--- a/rapida/components/landuse/search_utils/mgrsconv.py
+++ b/rapida/components/landuse/search_utils/mgrsconv.py
@@ -74,7 +74,7 @@ def _mgrs_100k_key_for_zone(lat: float, lon: float, zone: int) -> str:
def generate_utm_100k_grid(aoi_utm) -> List[Polygon]:
if aoi_utm.is_empty:
return []
- xmin, ymin, xmax, ymax = aoi_utm.bounds
+ xmin, ymin, xmax, ymax = aoi_utm.bounds_from_file
e0 = math.floor(xmin / 100000.0) * 100000.0
e1 = math.ceil (xmax / 100000.0) * 100000.0
n0 = math.floor(ymin / 100000.0) * 100000.0
diff --git a/rapida/components/landuse/stac_collection.py b/rapida/components/landuse/stac_collection.py
index be5373c..6b50360 100644
--- a/rapida/components/landuse/stac_collection.py
+++ b/rapida/components/landuse/stac_collection.py
@@ -265,7 +265,7 @@ def _merge_or_voronoi(self, df: gpd.GeoDataFrame, scene_size=110000) -> list[Pol
return [gpd.GeoSeries([merged], crs=3857).to_crs(original_crs).iloc[0]]
# Generate internal grid points spaced equally based on scene size
- minx, miny, maxx, maxy = merged.bounds
+ minx, miny, maxx, maxy = merged.bounds_from_file
# create double size of scene to be able to cover the whole scene area
spacing = scene_size * 2
nx = int(np.ceil((maxx - minx) / spacing))
diff --git a/rapida/components/landuse/utils.py b/rapida/components/landuse/utils.py
index 7150513..787cdbe 100644
--- a/rapida/components/landuse/utils.py
+++ b/rapida/components/landuse/utils.py
@@ -13,7 +13,7 @@
def _tile_id_from_center(ds) -> str:
# Use dataset center in WGS84 to derive the 100 km MGRS tile id (e.g., "36NYG")
- (minx, miny, maxx, maxy) = ds.bounds
+ (minx, miny, maxx, maxy) = ds.bounds_from_file
cx = (minx + maxx) / 2
cy = (miny + maxy) / 2
# to lon/lat
diff --git a/rapida/components/ntl/__init__.py b/rapida/components/ntl/__init__.py
new file mode 100644
index 0000000..29224ad
--- /dev/null
+++ b/rapida/components/ntl/__init__.py
@@ -0,0 +1,60 @@
+from rapida.core.component import Component
+from rapida.core.variable import Variable
+from rapida.project.project import Project
+from rapida.session import Session
+import logging
+import os
+
+logger = logging.getLogger('rapida')
+
+class NtlComponent(Component):
+
+ def __call__(self, variables: list[str], **kwargs):
+ if not variables:
+ variables = self.variables
+ else:
+ for var_name in variables:
+ if var_name not in self.variables:
+ logger.error(f'variable "{var_name}" is invalid. Valid options are "{", ".join(self.variables)}"')
+ return
+
+ with Session() as session:
+ variable_data = session.get_component(self.component_name)
+
+ for var_name in variables:
+ var_data = variable_data[var_name]
+
+ v = NTLVariable(
+ name=var_name,
+ component=self.component_name,
+
+ **var_data
+ )
+ v(**kwargs)
+
+
+class NTLVariable(Variable):
+
+
+ def __init__(self, **kwargs):
+
+ super().__init__(**kwargs)
+ project = Project(path=os.getcwd())
+ geopackage_path = project.geopackage_file_path
+ output_filename = f"{self.name}.tif"
+ self.local_path = os.path.join(os.path.dirname(geopackage_path), self.component, output_filename)
+
+ def download(self,force=False, **kwargs):
+ pass
+
+ def download(self, **kwargs):
+ pass
+ def resolve(self, **kwargs):
+ pass
+
+ def compute(self, **kwargs):
+ pass
+ def evaluate(self, **kwargs):
+ pass
+ def __call__(self, *args, **kwargs):
+ print(self.name)
\ No newline at end of file
diff --git a/rapida/components/ntl/variables.py b/rapida/components/ntl/variables.py
new file mode 100644
index 0000000..77b33fe
--- /dev/null
+++ b/rapida/components/ntl/variables.py
@@ -0,0 +1,41 @@
+from collections import OrderedDict
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+def generate_variables():
+ """
+ Generate relative wealth index variables dict
+ :return RWI variables definition
+ """
+
+ # https://www.earthdata.nasa.gov/data/catalog/sedac-ciesin-sedac-pmp-grdi-2010-2020-1.00
+ license = "Creative Commons Zero (CC0 1.0)"
+ attribution = "NASA Black Marble data courtesy of the NASA Goddard Space Flight Center’s Terrestrial Information Systems Laboratory and the Earth from Space Institute (EfSI)."
+
+ variables = OrderedDict()
+ variables['noaa_outage'] = dict(title='Outage detected through NOAA real time data',
+ source=f"NOAA",
+ operator='sum',
+ percentage=True,
+ license=license,
+ attribution="Data sourced from the NOAA Open Data Dissemination (NODD) Program, utilizing the Joint Polar Satellite System (JPSS) VIIRS Sensor Data Records hosted on [AWS Open Data / Google Cloud Public Datasets].",
+ )
+ variables['nasa_nrt_outage'] = dict(title='Outage detected through NASA Black Marble operational (LANCEMODIS) data',
+ source=f"NASA",
+ operator='sum',
+ percentage=True,
+ license=license,
+ attribution=attribution,
+ )
+
+ variables['nasa_outage'] = dict(title='Outage detected through NASA Black Marble archived (LAADS) data',
+ source=f"NASA",
+ operator='sum',
+ percentage=True,
+ license=license,
+ attribution=attribution,
+ )
+
+ return variables
\ No newline at end of file
diff --git a/rapida/ntl/cache.py b/rapida/ntl/cache.py
index 9429c1d..c9fa62e 100644
--- a/rapida/ntl/cache.py
+++ b/rapida/ntl/cache.py
@@ -8,7 +8,7 @@
MAX_AGE_SECONDS = 6 * 3600 # 6 hours
-CACHE_PATH = os.path.join(tempfile.gettempdir(), "ntl_search_cache")
+CACHE_PATH = os.path.join(tempfile.gettempdir(), "ntl_cache")
@@ -32,9 +32,10 @@ def store(key:str=None, value:str=None, tile:str=None, cache_path=CACHE_PATH):
tiles, creation_time = record
if tile:
if not tile in tiles:
- record[0].update({tile:value})
+ tiles.update({tile: value})
+ record = tiles, creation_time
else:
- record[0] = value
+ record = value, creation_time
cache[key] = record
@@ -48,16 +49,31 @@ def fetch(key:str=None, tile:str=None, cache_path=CACHE_PATH):
return
# 1. Directly unpack the tuple
tiles, creation_time = record
-
# 2. Check for expiration
if time.time() - creation_time > MAX_AGE_SECONDS:
del cache[key]
return # Expired
# 3. Handle the tile request
- if tile and tile in tiles:
- return tiles[tile],
- if isinstance(tiles, dict):
- return tuple(tiles.values())
+ if tile:
+ if tile in tiles:
+ return tiles[tile]
+ return
else:
return tiles
+
+
+if __name__ == '__main__':
+ key = 'VJ146A3_202604'
+ r = fetch(key=key, tile='h21v04')
+ print(r)
+ r = fetch(key=key)
+ print(r)
+ ky = '32445566'
+
+ store(key=ky, value='a')
+ r = fetch(key=ky)
+ print(r)
+ store(key=ky, value='b')
+ r = fetch(key=ky)
+ print(r)
\ No newline at end of file
diff --git a/rapida/ntl/fetch.py b/rapida/ntl/fetch.py
new file mode 100644
index 0000000..37d435d
--- /dev/null
+++ b/rapida/ntl/fetch.py
@@ -0,0 +1,159 @@
+
+from datetime import datetime
+import numbers
+import logging
+from rich.progress import Progress
+from rapida.components.ntl.variables import generate_variables
+from rapida.ntl.utils import get_intersecting_tiles
+from rapida.ntl.nasa.search import search
+from rapida.ntl.nasa import const as nasaconst
+from rapida.ntl.noaa.search import async_search_granules
+from rapida.ntl.noaa.cmask import select_required_granules
+from rapida.ntl.nasa.io import download as download_from_nasa
+from rapida.ntl.noaa.io import download as download_from_noaa
+import asyncio
+DELIVERABLES = tuple([g.upper() for g in generate_variables()])
+
+logger = logging.getLogger('rapida')
+
+async def download_and_track(granule, dest_dir, prog_bar):
+ # Run the actual download
+ results = await download_from_noaa(
+ satellite=granule.sat,
+ timestamp=granule.timestamp,
+ dst_dir=dest_dir,
+ progress=prog_bar
+ )
+ result_dict = {}
+ # Return both the timestamp AND the result
+ for e in results:
+ result_dict[e[0]] = e[1]
+ return granule.timestamp, result_dict
+
+async def fetch(bbox:tuple[numbers.Number]=None, nominal_date:datetime=None,
+ deliverable:str=None, dst_dir:str=None, progress:Progress=None):
+ """
+ Indentify and download the BEST available data suitable to detect outages.
+ :param dst_dir:
+ :param bbox:
+ :param nominal_date:
+ :param progress:
+ :param deliverable
+ :return:
+ """
+ deliverable = deliverable.lower()
+
+
+ if 'noaa' in deliverable: # operational real time data
+ logger.info(f'Going to predict VIIRS satellite passes for {nominal_date.date()} over target area: {bbox}')
+ granules = await async_search_granules(
+ satellites=None, nominal_date=nominal_date, bbox=bbox,
+ cmask=True, progress=progress)
+ if not granules:
+ logger.info(f'No descending granules for found for {nominal_date.date()} over target area {bbox}')
+ return
+ logger.info(f'Found {len(granules)} descending granules')
+ selected_granules = select_required_granules(sorted_granules=granules, bbox=bbox, progress=progress)
+ logger.info(f'Selected {len(selected_granules)} granule(s) that cover(s) bbox {bbox}')
+ tasks = []
+ for granule in selected_granules:
+ # We no longer need a dictionary, just a simple list of tasks
+ task = asyncio.create_task(
+ download_and_track(granule, dst_dir, progress)
+ )
+ tasks.append(task)
+
+ downloaded_files = {}
+
+ for coro in asyncio.as_completed(tasks, timeout=100 * 3 * len(tasks)):
+ try:
+ # Unpack the tuple we returned from our wrapper
+ timestamp, downloaded_files_dict = await coro
+ downloaded_files[timestamp] = downloaded_files_dict
+ logger.info(f'Downloaded operational VIIRS images for timestamp {timestamp}')
+ except Exception as e:
+ logger.error(e)
+
+ except asyncio.CancelledError as ce:
+ for atask in tasks:
+ if not atask.done():
+ atask.cancel()
+ await asyncio.gather(*tasks, return_exceptions=True)
+ raise
+
+ return downloaded_files
+
+ expected_tiles = get_intersecting_tiles(bbox=bbox)
+ routes = nasaconst.ROUTES
+ stream = nasaconst.ARCHIVE
+ if deliverable == 'baseline':
+ processing_levels = ['A3']
+ if 'nasa' in deliverable: #data from NASA LAADS catalogs
+ processing_levels = 'A2', 'A1' # best daily data ???
+ if 'nrt' in deliverable:
+ stream = nasaconst.OPERATIONAL
+
+ downloaded_files = {}
+
+ for processing_level in processing_levels:
+ for route in routes:
+
+ found_urls = search(
+ processing_level=processing_level,
+ nominal_date=nominal_date,
+ bbox=bbox,
+ stream=stream,
+ route=route,
+ progress=progress,
+ push_to_cache=True
+ )
+
+ if not found_urls:
+ logger.debug(
+ f'No data was found for deliverable {deliverable} at processing level {processing_level} through route {route}')
+ continue
+
+ products = tuple(sorted(set([e[0] for e in found_urls])))
+ selected_product = products[0]
+
+ # Simplified dictionary comprehension
+ selected_urls = {e[-1]: e[1] for e in found_urls if e[0] == selected_product}
+
+ if len(selected_urls) != len(expected_tiles):
+ logger.info(
+ f'Expected to get {len(expected_tiles)} for {selected_product} stream {stream} and processing level {processing_level} over route {route}. Got {len(selected_urls)}')
+ continue
+
+ logger.info(
+ f'Selected {len(selected_urls)} images for deliverable {deliverable} for processing level {processing_level} route {route}')
+
+ timestamps = set(selected_urls.values())
+ if len(timestamps) > 1:
+ logger.info(
+ f'Got more than one timestamp for for {selected_product} stream {stream} and processing level {processing_level} over route {route} ')
+ continue
+
+ timestamp, *_ = timestamps
+ urls = list(selected_urls)
+
+ downloaded = await download_from_nasa(timestamp=timestamp, product=selected_product, dst_dir=dst_dir, urls=urls,
+ progress=progress)
+
+
+ logger.info(f'Successfully downloaded {len(downloaded[timestamp])} selected images ')
+
+ if not timestamp in downloaded_files:
+ downloaded_files[timestamp] = {}
+
+
+ downloaded_files[timestamp].update({selected_product:downloaded[timestamp]})
+
+ # Break out of the 'routes' loop to stop searching for this processing_level
+ break
+
+
+ return downloaded_files
+
+
+
+
diff --git a/rapida/ntl/nasa/const.py b/rapida/ntl/nasa/const.py
index 2ea5c6c..3958562 100644
--- a/rapida/ntl/nasa/const.py
+++ b/rapida/ntl/nasa/const.py
@@ -59,7 +59,7 @@
"A1G": "DNB_At_Sensor_Radiance",
"A2": "DNB_BRDF-Corrected_NTL",
"A3": "AllAngle_Composite_Snow_Free",
- "A4": "NearNadir_Composite_Snow_Free"
+ "A4": "AllAngle_Composite_Snow_Free"
}
PROCESSING_LEVELS = {stream_name: list(stream_data.keys()) for stream_name, stream_data in COLLECTIONS.items()}
PROCESSING_LEVEL_NAMES = {CATALOG2STREAM[stream_name]: list(stream_data.keys()) for stream_name, stream_data in COLLECTIONS.items()}
diff --git a/rapida/ntl/nasa/io.py b/rapida/ntl/nasa/io.py
index 0dd5140..dedda70 100644
--- a/rapida/ntl/nasa/io.py
+++ b/rapida/ntl/nasa/io.py
@@ -1,6 +1,11 @@
from pathlib import Path
+
+import click.exceptions
import httpx
-from rapida.ntl.nasa.util import get_intersecting_tiles
+from osgeo import gdal, gdal_array
+import fsspec
+import h5py
+import secrets
import asyncio
import os
from rich.progress import Progress
@@ -9,22 +14,306 @@
import logging
from urllib.parse import urlparse
from rapida.ntl.nasa.search import stac_search
+from rapida.ntl.utils import get_intersecting_tiles
from datetime import datetime
import numbers
+from rapida.ntl.nasa import const as nasaconst
+from rapida.util.geo import gdal_callback
+import urllib
logger = logging.getLogger(__name__)
-async def download(timestamp: str = None, product: str = None, tile:str=None, dst_dir:str=None, progress:Progress=None):
- key = f'{product.upper()}_{timestamp}'
- urls = cache.fetch(key=key, tile=tile)
+def h52vrt(
+ paths: list[str],
+ sds_name: str,
+ is_remote: bool = False,
+ vrt_path: str = None,
+ bbox: tuple[float, float, float, float] = None
+) -> str:
+ """
+ Creates an in-memory/local disk mosaicked VRT from an iterable of local or remote VIIRS HDF5 files.
+ Bypasses GDAL's internal georeferencing failures by building explicit XML.
+ """
+ if not paths:
+ raise ValueError("The paths iterable cannot be empty.")
+
+ # 1. Setup Disk-less Auth (RAM only)
+ storage_options = {}
+ header_file_path = "/vsimem/gdal_auth.txt"
+
+ if is_remote:
+ token = os.environ.get('EARTHDATA_TOKEN')
+ # Add this sanity check
+ if not token:
+ raise ValueError("CRITICAL: EARTHDATA_TOKEN environment variable is not set or is empty!")
+ storage_options = {
+ "block_size": 1024 * 1024,
+ "headers": {"Authorization": f"Bearer {token}"}
+ }
+ # Inject the header file directly into GDAL's virtual memory
+ gdal.FileFromMemBuffer(header_file_path, f"Authorization: Bearer {token}\r\n".encode('utf-8'))
+
+ hdf_root = "HDFEOS/GRIDS/VIIRS_Grid_DNB_2d/Data_Fields"
+ dataset_path = f"{hdf_root}/{sds_name}"
+
+ tile_vrts = []
+ global_metadata = {}
+ # 2. Process each path to create geographically aware sub-VRTs
+ for i, path in enumerate(paths):
+ # Use fsspec to natively handle both local OS files and HTTP streams
+ with fsspec.open(path, "rb", **(storage_options if is_remote else {})) as f:
+ with h5py.File(f, 'r') as hfile:
+
+ # Extract Extent (Required for EVERY tile to mosaic correctly)
+ def get_val(key):
+ v = hfile.attrs[key]
+ return float(v[0]) if isinstance(v, (list, tuple)) or hasattr(v, '__iter__') else float(v)
+
+ west = get_val('WestBoundingCoord')
+ north = get_val('NorthBoundingCoord')
+ east = get_val('EastBoundingCoord')
+ south = get_val('SouthBoundingCoord')
+
+ # Extract Dimension & Metadata ONLY from the FIRST file
+ if i == 0:
+ try:
+ target_ds = hfile[dataset_path]
+ except KeyError as ke:
+ target_ds = hfile[dataset_path.replace('Data_Fields', 'Data Fields')]
+
+ height, width = target_ds.shape[-2:]
+
+ ds_dtype = gdal_array.NumericTypeCodeToGDALTypeCode(target_ds.dtype)
+ ds_dtype_name = gdal.GetDataTypeName(ds_dtype)
+
+ for k, v in target_ds.attrs.items():
+ if isinstance(v, bytes):
+ v = v.decode('utf-8')
+ elif hasattr(v, '__iter__') and not isinstance(v, str):
+ v = v[0].decode('utf-8') if isinstance(v[0], bytes) else str(v[0])
+ global_metadata[k] = str(v)
+ raw_fill = target_ds.attrs.get('_FillValue')
+ nodata_value = target_ds.dtype.type(raw_fill).item()
+
+ # Calculate Geotransform for THIS specific tile
+ px_w = (east - west) / width
+ px_h = (south - north) / height
+
+ # Construct GDAL connection string
+ if is_remote:
+ encoded = urllib.parse.quote(path, safe="")
+ auth_str = f"&header_file={header_file_path}" if token else ""
+ vsi = f"/vsicurl?empty_dir=yes{auth_str}&url={encoded}"
+ conn_str = f'HDF5:"{vsi}"://{dataset_path}'
+ else:
+ abs_path = os.path.abspath(path)
+ conn_str = f'HDF5:"{abs_path}"://{dataset_path}'
+ _, file_name = os.path.split(path)
+
+ # XML Template (EPSG:4326 is the immutable standard for VIIRS L3/L4)
+ tile_xml = f"""
+ EPSG:4326
+ {west}, {px_w}, 0.0, {north}, 0.0, {px_h}
+
+ {nodata_value}
+
+ {conn_str}
+ 1
+
+
+
+
+ """
+
+ # Save individual tile VRT to memory
+ tile_vrt_path = f"/vsimem/{file_name}{secrets.token_hex(6)}.vrt"
+ gdal.FileFromMemBuffer(tile_vrt_path, tile_xml.encode('utf-8'))
+ tile_vrts.append(tile_vrt_path)
+
+ # 3. Mosaic all virtual tiles into the final master VRT
+ vrt_opts = gdal.BuildVRTOptions(
+ outputBounds=bbox,
+ outputSRS='EPSG:4326',
+ resolution='highest',
+ resampleAlg=gdal.GRA_NearestNeighbour,
+ srcNodata=nodata_value,
+ VRTNodata=nodata_value
+ )
+
+ master_ds = gdal.BuildVRT(vrt_path, tile_vrts, options=vrt_opts)
+ if master_ds is None:
+ raise RuntimeError("GDAL failed to build the master VRT.")
+
+ # 4. Inject the unified metadata & explicitly set NoData
+ band = master_ds.GetRasterBand(1)
+ band.SetMetadata(global_metadata)
+ band.SetNoDataValue(nodata_value)
+
+ master_ds.FlushCache()
+ master_ds = None
+
+ if is_remote:
+ tile_vrts += [vrt_path, header_file_path]
+
+ else:
+ tile_vrts += [vrt_path]
+ return tile_vrts
+
+
+def extract_bb(image_files: list[str] = None, sds_name:str=None, return_gt=False,
+ bbox: tuple[float, float, float, float] = None, progress=None
+ ):
+ vrt_path = f'/vsimem/{secrets.token_hex(10)}.vrt'
+ to_unlink = []
+ to_unlink += h52vrt(paths=image_files, sds_name=sds_name, is_remote=False, bbox=bbox,vrt_path=vrt_path)
+ to_unlink = [e for e in to_unlink if gdal.VSIStatL(e) is not None]
+ translate_options = dict(
+ format="MEM",
+ outputSRS='EPSG:4326',
+
+ )
+
+ if progress:
+ task = progress.add_task(f'[red]Extracting bbox {bbox} using GDAL')
+ callback_dict = dict(
+ callback=gdal_callback,
+ callback_data=(progress, task, None)
+ )
+ translate_options.update(callback_dict)
+
+ # Pass the dataset object directly instead of the string path
+ ds = gdal.Translate(destName='', srcDS=vrt_path, **translate_options)
+
+ gt = ds.GetGeoTransform()
+ [gdal.Unlink(e) for e in to_unlink]
+ band = ds.GetRasterBand(1)
+ array = band.ReadAsArray()
+
+
+ ds = None
+ if return_gt:
+ return array, gt
+ return array
+
+async def extract(image_files: list[str] = None, sds_name:str=None, product:str=None, dst_dir:str=None,
+ nominal_date:datetime=None, bbox: tuple[float, float, float, float] = None, progress=None
+ ):
+ product_tif_path = os.path.join(dst_dir, f'{product}_{nominal_date:%Y%m%d}_{nominal_date.timestamp()}.tif')
+ _, tif_name = os.path.split(product_tif_path)
+
+ vrt_path = f'/vsimem/{tif_name}.vrt'
+ to_unlink = []
+ to_unlink += h52vrt(paths=image_files, sds_name=sds_name, is_remote=False, bbox=bbox,vrt_path=vrt_path)
+ to_unlink = [e for e in to_unlink if gdal.VSIStatL(e) is not None]
+ translate_options = dict(
+ format="GTiff",
+ creationOptions=[
+ "TILED=YES", # Optimizes read performance
+ "BIGTIFF=IF_SAFER",
+ "COPY_SRC_MDD=YES"
+ ],
+ outputSRS='EPSG:4326',
+
+ )
+
+ if progress:
+ task = progress.add_task(f'[red]Extracting data using GDAL')
+ callback_dict = dict(
+ callback=gdal_callback,
+ callback_data=(progress, task, None)
+ )
+ translate_options.update(callback_dict)
+
+ # Pass the dataset object directly instead of the string path
+ ds = gdal.Translate(destName=product_tif_path, srcDS=vrt_path, **translate_options)
+ ds = None
+
+ [gdal.Unlink(e) for e in to_unlink]
+ return Path(product_tif_path)
+
+async def download_and_extract(file_urls: list[str] = None, stream: str = None, route=None, processing_level=None,
+ product: str = None, bbox: tuple[float, float, float, float] = None, vsimem=False,
+ dst_dir: str = None, progress=None,
+ ):
+ timestamps = set(file_urls.values())
+ if len(timestamps) > 1:
+ logger.info(
+ f'Got more than one timestamp for for {product} stream {stream} and processing level {processing_level} over route {route} ')
+ return
+ timestamp, = timestamps
+ urls = list(file_urls)
+ sds_name = nasaconst.SUB_DATASETS.get(processing_level)
+ if not sds_name:
+ raise ValueError(f"Processing level '{processing_level}' not found in mapping.")
+ vrt_path = f'/vsimem/{product}_{timestamp}_{secrets.token_hex(6)}.vrt'
+ to_unlink = []
+ if not vsimem:
+ downloaded_files = await download(timestamp=timestamp, product=product, dst_dir=dst_dir, urls=urls,
+ progress=progress)
+ to_unlink += h52vrt(paths=downloaded_files, sds_name=sds_name, is_remote=False, bbox=bbox,
+ vrt_path=vrt_path)
+ else:
+ to_unlink += h52vrt(paths=urls, sds_name=sds_name, is_remote=True, bbox=bbox, vrt_path=vrt_path)
+
+ to_unlink = [e for e in to_unlink if gdal.VSIStatL(e) is not None]
+ _, vrt_name = os.path.split(vrt_path)
+ output_tif_path: str = os.path.join(dst_dir, vrt_name.replace('.vrt', '.tif'))
+
+ translate_options = dict(
+ format="GTiff",
+ creationOptions=[
+ "TILED=YES", # Optimizes read performance
+ "BIGTIFF=IF_SAFER",
+ "COPY_SRC_MDD=YES"
+ ],
+ outputSRS='EPSG:4326',
+
+ )
+
+ if progress:
+ task = progress.add_task(f'[red]Extracting data using GDAL')
+ callback_dict = dict(
+ callback=gdal_callback,
+ callback_data=(progress, task, None)
+ )
+ translate_options.update(callback_dict)
+
+ # Pass the dataset object directly instead of the string path
+ ds = gdal.Translate(destName=output_tif_path, srcDS=vrt_path, **translate_options)
+ ds = None
+
+ [gdal.Unlink(e) for e in to_unlink]
+ return output_tif_path
+
+
+
+async def download(timestamp: str = None, product: str = None, tile:str=None,
+ bbox:tuple[float, float, float, float]=None,dst_dir:str=None, urls:list[str]=[], progress:Progress=None):
+
+ assert timestamp not in [None, ''], f'Invalid timestamp={timestamp}'
+ assert product not in [None, ''], f'Invalid product={product}'
+ if bbox is None and tile is None and not urls:
+ raise click.exceptions.BadOptionUsage(message=f'Either bbox ot tile is required for download.')
+
if not urls:
- logger.info(f'Failed to locate information in {cache.CACHE_PATH} for {product}-{timestamp}-{tile or ""} \n' \
- f'Consider searching first.')
- return
+ if tile is None:
+ assert bbox, f'Invalid bbox={bbox}'
+ if tile is None and bbox is not None:
+ tiles = get_intersecting_tiles(bbox=bbox)
+ else:
+ tiles = tile,
+ key = f'{product.upper()}_{timestamp}'
+ for tile in tiles:
+ urls.append(cache.fetch(key=key, tile=tile))
+
+ if not len(urls) == len(tiles):
+ logger.info(f'Failed to locate information in {cache.CACHE_PATH} for {product}-{timestamp}-{tile or ""} \n' \
+ f'Consider searching first.')
+ return
# EarthAccess token
ea_token = os.environ.get('EARTHDATA_TOKEN')
@@ -34,9 +323,11 @@ async def download(timestamp: str = None, product: str = None, tile:str=None, ds
raise ValueError("CRITICAL: EARTHDATA_TOKEN environment variable is not set or is empty!")
headers = {"Authorization": f"Bearer {ea_token}"}
- return await download_remote_files(
+
+ downloaded_files = await download_remote_files(
file_urls=urls,dst_folder=dst_dir, progress=progress, headers=headers
)
+ return {timestamp:downloaded_files}
async def download_tile(
diff --git a/rapida/ntl/nasa/search.py b/rapida/ntl/nasa/search.py
index 89dbd83..5ea3d6f 100644
--- a/rapida/ntl/nasa/search.py
+++ b/rapida/ntl/nasa/search.py
@@ -1,16 +1,16 @@
-import json
import os.path
from rapida.ntl import cache
from rapida.ntl.nasa import const
-from rapida.ntl.nasa.util import timestamp_format
+from rapida.ntl.utils import timestamp_format
import math
-from datetime import datetime, timedelta, date
+from datetime import datetime, timedelta
import logging
from pystac_client import Client
from rich.progress import Progress
-from rapida.ntl.nasa.util import get_intersecting_tiles
+from rapida.ntl.utils import get_intersecting_tiles
import httpx
from typing import Optional
+from rapida.util.http_get_json import http_get_json
logger = logging.getLogger(__name__)
@@ -42,7 +42,7 @@ def calculate_night_hours(midlat: float, day_of_year: int) -> int:
return int(round(night_hrs))
-def url2result(url:str=None, store=True):
+def url2result(url:str, store=True):
_, file_name = os.path.split(url)
match = const.NTL_FILENAME_PATTERN.match(file_name)
@@ -58,7 +58,8 @@ def url2result(url:str=None, store=True):
-def calculate_local_utc(stream:str, processing_level:str, nominal_date: datetime, bbox:tuple[float], route:str=None):
+def calculate_local_utc_old(stream:str, processing_level:str, nominal_date: datetime, bbox:tuple[float, float, float, float],
+ route:str=None, products:list[str] =None):
"""
Calculate VIIRS satellites local overpass time in UTC TZ
:param stream:
@@ -85,13 +86,28 @@ def calculate_local_utc(stream:str, processing_level:str, nominal_date: datetime
dt = local_overpass_utc
elif 'A3' in plevel:
- day = 15 if route == 'STAC' else 1
- # A3 Monthly composites target mid-month. If current month, step back one month.
- if now.month == nominal_date.month and now.year == nominal_date.year:
- prev_month = nominal_date.replace(day=1) - timedelta(days=1)
- dt = prev_month.replace(day=day)
+
+ if route == 'STAC':
+ day=15
+ # A3 Monthly composites target mid-month. If current month, step back one month.
+ if now.month == nominal_date.month and now.year == nominal_date.year:
+ dt = nominal_date.replace(month=now.month-1, day=day)
+ else:
+ dt = nominal_date.replace(day=day)
else:
- dt = nominal_date.replace(day=day)
+ months_back = 3
+ day = 1
+ for m in range(1, months_back+1):
+ exists = True
+ for product in products:
+ dt = nominal_date.replace(month=now.month - m, day=day)
+ content_url = f'{const.API_CONTENT[stream]}/{product}/{dt.strftime("%Y/%j")}'
+ try:
+ content = http_get_json(url=content_url, timeout=10)
+ exists &= content is not None
+ except httpx.HTTPStatusError:
+ exists = False
+ if exists:break
elif 'A4' in plevel:
if now.year > nominal_date.year:
raise ValueError(f'Can not search in future! Please adjust target date')
@@ -102,19 +118,158 @@ def calculate_local_utc(stream:str, processing_level:str, nominal_date: datetime
dt = nominal_date.replace(month=month, day=1)
else:
raise ValueError(f'Invalid stream {stream} for NASA NTL data')
+
return dt
-def api_search(stream:str, products:str, dt:datetime, bbox:tuple[float], push_to_cache:bool=True)-> list[str]:
+def stac_catalog_has_data(stream: str, processing_level: str, dt: datetime,
+ bbox: tuple[float, float, float, float], products: list[str] = None) -> bool:
+ """
+ Pure 1:1 clone of stac_search logic. Hardened to expose local import errors.
+ """
+ catalog_name = const.STREAM2CATALOG[stream]
+ catalog_collections = const.COLLECTIONS[catalog_name]
+
+ if not products:
+ available_collections = sorted(catalog_collections[processing_level.upper()], reverse=True)
+ else:
+ available_collections = list(products)
+
+ stac_url = f'{const.CMR_STAC_ROOT}{catalog_name}'
+
+ try:
+ # If Client is not imported, this will instantly throw a NameError
+ catalog = Client.open(url=stac_url)
+
+ search_result = catalog.search(
+ collections=available_collections,
+ datetime=dt, # Exact match, identical to your stac_search
+ bbox=bbox
+ )
+
+ if search_result.matched():
+ expected_tiles = get_intersecting_tiles(bbox=bbox)
+ items = search_result.item_collection()
+
+ for itm in items:
+ for asset_key, asset in itm.assets.items():
+ if asset.href.endswith('.h5') and asset.href.startswith(('https', 'http')):
+ _, file_name = os.path.split(asset.href)
+ match = const.NTL_FILENAME_PATTERN.match(file_name)
+ if match:
+ meta = match.groupdict()
+ if meta['tile'] in expected_tiles:
+ return True
+
+ return False
+
+ except Exception as e:
+ # ELEVATED TO ERROR: This will print the exact Python crash to your console
+ logger.error(f"CRITICAL STAC PROBE FAILURE for {dt.strftime('%Y-%m')}: {e}", exc_info=True)
+ return False
+
+
+def calculate_local_utc(stream: str, processing_level: str, nominal_date: datetime,
+ bbox: tuple[float, float, float, float],
+ route: str = None, products: list[str] = None):
+ """
+ Calculate VIIRS satellites local overpass time in UTC TZ
+ """
+ minlon, minlat, maxlon, maxlat = bbox
+ now = datetime.now()
+ plevel = processing_level.upper()
+
+ if stream == const.OPERATIONAL:
+ days_difference = abs((now - nominal_date).days)
+ if days_difference > 7:
+ raise ValueError(f'Invalid target_date={nominal_date}. {stream} stream holds max 7 days of data.')
+
+ if 'A1' in plevel or 'A2' in plevel:
+ midlon = (minlon + maxlon) * 0.5
+ utc_offset_hours = midlon / 15.0
+ local_overpass_utc = nominal_date + timedelta(hours=1.5) - timedelta(hours=utc_offset_hours)
+ dt = local_overpass_utc
+
+ elif 'A3' in plevel:
+ months_back = 3
+ day = 15 if route == 'STAC' else 1
+ current_probe_date = nominal_date
+
+ if now.year == current_probe_date.year and now.month == current_probe_date.month:
+ current_probe_date = current_probe_date.replace(day=1) - timedelta(days=1)
+
+ found_valid_dt = False
+
+ for attempt in range(months_back + 1):
+ dt = current_probe_date.replace(day=day)
+ exists = True
+
+ if route == 'API':
+ for product in products:
+ content_url = f'{const.API_CONTENT[stream]}/{product}/{dt.strftime("%Y/%j")}'
+ try:
+ content = http_get_json(url=content_url, timeout=10.0)
+ if not content or len(content) == 0:
+ exists = False
+ break
+ except Exception as e:
+ logger.debug(f"API Probe failed for {product} on {dt.strftime('%Y-%m')}: {e}")
+ exists = False
+ break
+
+ elif route == 'STAC':
+ # Symmetrical STAC Probe using the dynamic URL & spatial filter helper
+ exists = stac_catalog_has_data(
+ stream=stream,
+ processing_level=plevel,
+ dt=dt,
+ bbox=bbox,
+ products=None # Passing None triggers the automatic const fallback just like stac_search
+ )
+
+ if exists:
+ found_valid_dt = True
+ break
+ else:
+ logger.debug(f"No {route} data found for {current_probe_date.strftime('%Y-%m')}. Stepping back.")
+ current_probe_date = current_probe_date.replace(day=1) - timedelta(days=1)
+
+ if not found_valid_dt:
+ logger.warning(
+ f"Catastrophic latency: Could not find published A3 data via {route} within {months_back} months of {nominal_date.strftime('%Y-%m')}."
+ )
+ dt = nominal_date.replace(day=day)
+
+ elif 'A4' in plevel:
+ if nominal_date > now:
+ raise ValueError('Cannot search in the future! Please adjust target date.')
+
+ month = 7 if route == 'STAC' else 1
+
+ if now.year == nominal_date.year:
+ dt = nominal_date.replace(year=nominal_date.year - 1, month=month, day=1)
+ else:
+ dt = nominal_date.replace(month=month, day=1)
+
+ else:
+ raise ValueError(f'Invalid stream {stream} for NASA NTL data')
+
+ return dt
+
+def api_search(stream:str, products:str, dt:datetime, bbox:tuple[float, float, float, float], push_to_cache:bool=True)-> list[str]:
tiles = get_intersecting_tiles(bbox=bbox)
urls = []
+ logger.info(
+ f'Searching for imagery in products "{products}')
for product in products:
-
content_url = f'{const.API_CONTENT[stream]}/{product}/{dt.strftime("%Y/%j")}'
with httpx.Client() as client:
# Fetch the JSON directory listing
response = client.get(content_url, timeout=10.0)
- response.raise_for_status()
+ try:
+ response.raise_for_status()
+ except httpx.HTTPStatusError:
+ continue
# MODAPS returns a flat JSON array of file objects
payload = response.json()['content']
for item in payload:
@@ -129,7 +284,7 @@ def api_search(stream:str, products:str, dt:datetime, bbox:tuple[float], push_to
return urls
def stac_search(stream:str=None, processing_level:Optional[str]=None, products:list[str]=None, dt:datetime=None,
- bbox:tuple[float]=None, push_to_cache:bool=True):
+ bbox:tuple[float, float, float, float]=None, push_to_cache:bool=True):
catalog_name = const.STREAM2CATALOG[stream]
catalog_collections = const.COLLECTIONS[catalog_name]
@@ -162,14 +317,21 @@ def stac_search(stream:str=None, processing_level:Optional[str]=None, products:l
if search_result.matched():
logger.info(f"Found {search_result.matched()} granule(s) at {stac_url}")
+ expected_tiles = get_intersecting_tiles(bbox=bbox)
items = search_result.item_collection()
for itm in items:
#print(json.dumps(itm.to_dict(), indent=4))
for asset_key, asset in itm.assets.items():
# Look for the .h5 file, but specifically grab the HTTPS link
- if asset.href.endswith('.h5') and asset.href.startswith('https'):
+ if asset.href.endswith('.h5') and asset.href.startswith(('https', 'http')):
url = asset.href
+ _, file_name = os.path.split(url)
+ match = const.NTL_FILENAME_PATTERN.match(file_name)
+ meta = match.groupdict()
+ tile = meta['tile']
+ if not tile in expected_tiles:
+ continue
result = url2result(url=url, store=push_to_cache)
urls.append(result)
@@ -197,26 +359,30 @@ def search(
assert processing_level.upper() in stream_processing_levels, (
f'Invalid processing level {processing_level} for {stream_name}. \''
f'Valid processing levels {stream_processing_levels}')
-
- dt = calculate_local_utc(stream=stream,processing_level=processing_level,
- nominal_date=nominal_date, bbox=bbox, route=route)
products = stream_products[processing_level]
+ dt = calculate_local_utc(stream=stream,processing_level=processing_level,
+ nominal_date=nominal_date, bbox=bbox, route=route, products=products)
+ expected_tiles = get_intersecting_tiles(bbox=bbox)
+ expected_tiles_count = len(expected_tiles)
cached_results = []
expected_products_count = len(products)
found_products_count = 0
-
+ found_tiles_count = 0
+ keys = []
for product in products:
timestamp = dt.strftime(timestamp_format(product_id=product))
key = f'{product}_{timestamp}'
- urls = cache.fetch(key=key)
- if urls:
- found_products_count += 1
- for url in urls:
+ for tile in expected_tiles:
+ url = cache.fetch(key=key, tile=tile)
+ if url:
cached_results.append(url2result(url=url, store=False))
+ found_tiles_count += 1
+ found_products_count += 1
+ keys.append(key)
- # Only short-circuit if the cache successfully returned data for EVERY product requested
- if found_products_count == expected_products_count:
- logger.info("Full cache hit. Bypassing network search.")
+ # Only short-circuit if the cache successfully returned expected number of tiles for EVERY product requested
+ if found_products_count == expected_products_count and found_tiles_count >= expected_tiles_count*expected_products_count:
+ logger.info(f"Full cache hit for {keys}. Bypassing network search.")
return cached_results
# --- 2. Catalog Search ---
if progress:
@@ -230,6 +396,7 @@ def search(
products=products,
dt=dt,
bbox=bbox,
+ push_to_cache=push_to_cache
)
else:
urls = stac_search(
@@ -237,10 +404,11 @@ def search(
processing_level=processing_level,
dt=dt,
bbox=bbox,
+ push_to_cache=push_to_cache
)
if not urls:
- logger.info(f"No imagery found for {processing_level} on {nominal_date.date()}")
+ logger.info(f"No imagery found for stream {stream} route {route} level {processing_level} on {nominal_date.date()}")
return
else:
if progress and 'progress_task' in locals():
diff --git a/rapida/ntl/nasa/util.py b/rapida/ntl/nasa/util.py
deleted file mode 100644
index 3209d9d..0000000
--- a/rapida/ntl/nasa/util.py
+++ /dev/null
@@ -1,37 +0,0 @@
-import math
-def get_intersecting_tiles(bbox: tuple[float, float, float, float]) -> list[tuple[int, int]]:
- """
- Identifies VIIRS Sinusoidal tiles (h, v) intersecting a geographic bounding box.
- bbox format: (min_lon, min_lat, max_lon, max_lat)
- :return tuple of ints representing pairs of tile coordinates (horizontal, vertical)
- """
- min_lon, min_lat, max_lon, max_lat = bbox
-
- # VIIRS standard sinusoidal grid is approx 10x10 degrees at the equator
- # h runs 0 to 35 (180W to 180E)
- # v runs 0 to 17 (90N to 90S)
- h_min = math.floor((min_lon + 180) / 10)
- h_max = math.floor((max_lon + 180) / 10)
- v_min = math.floor((90 - max_lat) / 10)
- v_max = math.floor((90 - min_lat) / 10)
-
- tiles = []
- for v in range(max(0, v_min), min(18, v_max + 1)):
- for h in range(max(0, h_min), min(36, h_max + 1)):
- tiles.append(f'h{h:02d}v{v:02d}')
-
- return tiles
-
-TIMESTAMP_FORMATS = {
- "A1": "%Y%m%d", # Daily: Year + Julian Day (e.g., 2026134)
- "A2": "%Y%m%d", # Daily: Year + Julian Day (e.g., 2026134)
- "A3": "%Y%m", # Monthly: Year + Month (e.g., 202605)
- "A4": "%Y" # Yearly: Year only (e.g., 2026)
-}
-
-def timestamp_format(product_id: str) -> str:
- """Determine the correct temporal format string based on product name."""
- # Check if A1, A2, A3, or A4 is in the product string (e.g., 'VNP46A1')
- for identifier, time_format in TIMESTAMP_FORMATS.items():
- if identifier in product_id:
- return time_format
diff --git a/rapida/ntl/noaa/cmask.py b/rapida/ntl/noaa/cmask.py
index 63443c9..36bf7b7 100644
--- a/rapida/ntl/noaa/cmask.py
+++ b/rapida/ntl/noaa/cmask.py
@@ -13,6 +13,10 @@
from osgeo import gdal
from shapely.ops import transform
from rapida.ntl import cache
+from shapely.geometry import MultiPoint, Polygon
+from shapely import concave_hull
+from shapely.ops import unary_union
+from itertools import combinations
gdal.UseExceptions()
@@ -23,16 +27,101 @@ def shift_to_360(lon, lat, z=None):
shifted_lon = lon + 360.0 if lon < 0 else lon
return (shifted_lon, lat) if z is None else (shifted_lon, lat, z)
-def bbox_in_hdf(hdf_url: str, bbox: Iterable[float]):
- fs = fsspec.filesystem("http")
+def bounds_from_url(hdf_url: str):
+
purl = urllib.parse.urlparse(hdf_url)
_, filename = os.path.split(purl.path)
+ storage_options = {}
+ if hdf_url.startswith(('http', 'https')):
+ storage_options.update({'block_size': 1024 * 1024})
+ with fsspec.open(hdf_url, **storage_options) as f:
+ with h5py.File(f, "r") as hfile:
+ # Now it reads at HTTP speeds without the boto3 overhead
+ return bounds_from_file(hfile=hfile)
+
+
+def bounds_from_file(hfile, step=50) -> Polygon:
+ # 1. Subsample the entire 2D array directly during the read.
+ # h5py translates this slice into an optimized partial read,
+ # making it very fast even over a network (fsspec).
+ lat_grid = hfile['Latitude'][::step, ::step][()]
+ lon_grid = hfile['Longitude'][::step, ::step][()]
+
+ # 2. Flatten into 1D arrays
+ lats = lat_grid.flatten()
+ lons = lon_grid.flatten()
+
+ # 3. Create a mask to filter out NoData/Fill values globally
+ valid_mask = (lons >= -180.0) & (lons <= 180.0) & \
+ (lats >= -90.0) & (lats <= 90.0)
+
+ valid_lons = lons[valid_mask]
+ valid_lats = lats[valid_mask]
+
+ if len(valid_lons) == 0:
+ return Polygon() # Return empty if no valid data exists
+
+ # 4. Create a Shapely MultiPoint collection
+ points = MultiPoint(np.column_stack((valid_lons, valid_lats)))
- with fs.open(hdf_url, block_size=1024 * 1024) as f:
+ # 5. Generate the footprint boundary
+ # convex_hull creates a tight bounding polygon around the outermost points.
+ poly = concave_hull(points,ratio=0.1)
+
+ return poly.buffer(0)
+
+
+def select_required_granules(sorted_granules: list, bbox: tuple, progress:Progress=None) -> list:
+ boxpoly = box(*bbox, ccw=True)
+ best_poly = bounds_from_url(sorted_granules[0].url)
+ uncovered = boxpoly.difference(best_poly)
+ if uncovered.is_empty or uncovered.area < 1e-6:
+ logger.debug(f"BBOX {bbox} is covered by first granule.")
+ return [sorted_granules[0]]
+ logger.info(f'bbox {bbox} is not covered completely by best swath...going to select more for complete coverage.')
+ progress_task = None
+ try:
+ if progress:
+ progress_task = progress.add_task(description=f'Selecting best granules tha cover {bbox}', total=None)
+ for combo_size in range(2, len(sorted_granules) + 1):
+ if progress and progress_task:
+ progress.update(progress_task, description=f'Evaluating granules by pairs of {combo_size} ')
+ for combo in combinations(sorted_granules, combo_size):
+
+ # Merge the geometries for this specific combination
+ merged_poly = unary_union([bounds_from_url(g.url) for g in combo])
+
+ # 3. FLOATING-POINT SAFE COVERAGE CHECK
+ # We use difference() because .within() can fail on microscopic 1e-15 gap artifacts
+ uncovered = boxpoly.difference(merged_poly)
+
+ if uncovered.is_empty or uncovered.area < 1e-6:
+ logger.debug(f"Success: BBOX covered perfectly by {combo_size} granule(s).")
+ return combo # Exits immediately with the absolute minimum required set
+
+ logger.warning("Exhausted all combinations. BBOX cannot be fully covered by available data.")
+ return tuple()
+ finally:
+ if progress and progress_task:
+ progress.remove_task(progress_task)
+
+def bbox_in_hdf(hdf_url: str, bbox: Iterable[float]):
+ #fs = fsspec.filesystem("http")
+ purl = urllib.parse.urlparse(hdf_url)
+ dst_dir, filename = os.path.split(purl.path)
+ storage_options = {}
+ if hdf_url.startswith(('http', 'https')):
+ storage_options.update({'block_size':1024*1024})
+ with fsspec.open(hdf_url, **storage_options) as f:
with h5py.File(f, "r") as hfile:
# Now it reads at HTTP speeds without the boto3 overhead
- bounds_poly = wkt.loads(hfile.attrs['geospatial_bounds'].decode('utf-8'))
+ #bounds_poly = wkt.loads(hfile.attrs['geospatial_bounds'].decode('utf-8'))
+ bounds_poly = bounds_from_file(hfile)
bbox_poly = box(*bbox, ccw=True)
+ # with open(os.path.join('/tmp', 'bbox.geojson'), "w") as ff:
+ # ff.write(to_geojson(bbox_poly))
+ # with open(os.path.join('/tmp', filename.replace('.nc', '.geojson')), "w") as f:
+ # f.write(to_geojson(bounds_poly))
# 2. The Kiribati Ghost Detection
minx, miny, maxx, maxy = bounds_poly.bounds
is_idl_crosser = (maxx - minx) > 300
@@ -47,13 +136,10 @@ def bbox_in_hdf(hdf_url: str, bbox: Iterable[float]):
working_bbox = bbox_poly
#if not bbox_poly.within(bounds_poly):
if not working_bbox.intersects(working_bounds):
- return False
+ return False, 0
intersection_poly = working_bbox.intersection(working_bounds)
perc_intersection = round(intersection_poly.area/working_bbox.area * 100)
- # with open("/tmp/bbox.geojson", "w") as ff:
- # ff.write(to_geojson(bbox_poly))
- # n = filename.split('_')[3]
- # with open(f"/tmp/granule_{n}.geojson", "w") as f:
+ # with open(os.path.join('/tmp', filename.replace('.nc', '.geojson')), "w") as f:
# f.write(to_geojson(bounds_poly))
return True, perc_intersection
@@ -161,7 +247,10 @@ def cloud_coverage_fast(hdf_url: str, bbox: Iterable[float],
def cloud_coverage(hdf_url: str, bbox: list) -> int:
-
+ # 1. Initialize GDAL environment INSIDE the worker process
+ gdal.UseExceptions()
+ gdal.PushErrorHandler('CPLQuietErrorHandler')
+ gdal.SetConfigOption('GDAL_HTTP_TIMEOUT', '300') # Prevents hanging vsicurl requests
_, file_name = os.path.split(hdf_url)
cc = cache.fetch(key=file_name)
@@ -202,7 +291,7 @@ def cloud_coverage_batch(urls: list[str], bbox: Iterable[float], max_threads: in
if progress:
master_task = progress.add_task(
description=f"[cyan]Computing cloud coverage .... ",
- total=None
+ total=len(urls)
)
then = datetime.datetime.now()
@@ -231,3 +320,4 @@ def cloud_coverage_batch(urls: list[str], bbox: Iterable[float], max_threads: in
return results
+
diff --git a/rapida/ntl/noaa/const.py b/rapida/ntl/noaa/const.py
index 47f7d70..b5e4533 100644
--- a/rapida/ntl/noaa/const.py
+++ b/rapida/ntl/noaa/const.py
@@ -1,15 +1,19 @@
import re
+CM = 'CM'
+GEO = 'GEO'
+SDR = 'SDR'
+
PRODUCTS_RE = {
- 'CM': re.compile(r'^(?P[\w-]+)_(?Pv\d+r\d+)_(?P\w+)_s(?P\d+)_e(?P\d+)_c(?P\d+)\.(?P\w+)$'),
- 'GEO': re.compile(r'^(?P[^_]+)_(?P[^_]+)_d(?P\d{8})_t(?P\d+)_e(?P\d+)_b(?P\d+)_c(?P\d+)_(?P[^_]+)_(?P[^_]+)\.(?P\w+)$'),
- 'SDR': re.compile(r'^(?P[^_]+)_(?P[^_]+)_d(?P\d{8})_t(?P\d+)_e(?P\d+)_b(?P\d+)_c(?P\d+)_(?P[^_]+)_(?P[^_]+)\.(?P\w+)$')
+ CM: re.compile(r'^(?P[\w-]+)_(?Pv\d+r\d+)_(?P\w+)_s(?P\d+)_e(?P\d+)_c(?P\d+)\.(?P\w+)$'),
+ GEO: re.compile(r'^(?P[^_]+)_(?P[^_]+)_d(?P\d{8})_t(?P\d+)_e(?P\d+)_b(?P\d+)_c(?P\d+)_(?P[^_]+)_(?P[^_]+)\.(?P\w+)$'),
+ SDR: re.compile(r'^(?P[^_]+)_(?P[^_]+)_d(?P\d{8})_t(?P\d+)_e(?P\d+)_b(?P\d+)_c(?P\d+)_(?P[^_]+)_(?P[^_]+)\.(?P\w+)$')
}
PRODUCTS={
- 'SDR':"VIIRS-DNB-SDR",
- 'GEO':"VIIRS-DNB-GEO",
- 'CM':"VIIRS-JRR-CloudMask"
+ SDR:"VIIRS-DNB-SDR",
+ GEO:"VIIRS-DNB-GEO",
+ CM:"VIIRS-JRR-CloudMask"
}
PRODUCT2NAME = dict((v, k) for k, v in PRODUCTS.items())
diff --git a/rapida/ntl/noaa/io.py b/rapida/ntl/noaa/io.py
index b13aefb..3462e9a 100644
--- a/rapida/ntl/noaa/io.py
+++ b/rapida/ntl/noaa/io.py
@@ -10,7 +10,10 @@
from urllib.parse import urlparse
from rapida.ntl.noaa.const import PRODUCT2NAME
import aiofiles
-
+from pyresample import geometry
+from satpy import Scene
+import numpy as np
+from pathlib import Path
logger = logging.getLogger(__name__)
@@ -23,6 +26,17 @@
for sat, sources in VIIRS_URLS.items()
}
+
+def get_viirs_stores(satellite: str):
+ """
+ Factory function to instantiate obstore clients on demand.
+ Must only be called from inside an active asyncio event loop.
+ """
+ return {
+ source: obstore.store.from_url(url, config=PUBLIC_CONFIG)
+ for source, url in VIIRS_URLS[satellite].items()
+ }
+
def parse_noaa_timestamp(time_str: str) -> datetime:
"""
Converts a NOAA VIIRS string (e.g., '202604010001018') into a timezone-naive UTC datetime.
@@ -104,8 +118,9 @@ async def find_ntl(satellite: str = None, bbox: Iterable[float] = None, dt: date
public_file_url = public_url(file_path=file_path, satellite=satellite, source=current_source)
is_intersecting, percent = bbox_in_hdf(hdf_url=public_file_url,bbox=bbox)
if not is_intersecting:
+ _, file_name = os.path.split(file_path)
logger.info(
- f'Skipping {file_path} from {current_source} generated by {satellite} because it does not intersect {bbox} bbox')
+ f'Skipping {file_name} from {current_source} scanned by {satellite} because it does not intersect bbox')
spatial_misses.add(time_pattern)
continue
@@ -188,11 +203,11 @@ async def locate_file(satellite:str=None, dt=None, source:str=None, products: It
async def fetch_file(satellite:str=None, provider:str=None, path:str=None, size:int=None, dst_dir:str=None,
progress=None, progress_task = None):
+ down_task = None
try:
adir = os.path.abspath(dst_dir)
- if not os.path.exists(adir):
- os.mkdir(adir)
- down_task = None
+ os.makedirs(adir, exist_ok=True)
+
store = VIIRS_STORES[satellite][provider]
rel_path, file_name = os.path.split(path)
product = rel_path.split('/')[0]
@@ -201,6 +216,12 @@ async def fetch_file(satellite:str=None, provider:str=None, path:str=None, size:
down_task = progress.add_task(f'[red]Downloading {file_name} from {provider}', total=size)
dst_file_path = os.path.join(adir, file_name)
response = await obstore.get_async(store, path)
+
+ dst_path = Path(dst_file_path)
+ if dst_path.exists() and dst_path.stat().st_size == response.meta['size']:
+ if progress and progress_task is not None:
+ progress.update(progress_task, description=f'[green]Reused {file_name} from local folder {adir}', advance=1)
+ return product_name, dst_file_path, dst_path.stat().st_size
async with aiofiles.open(dst_file_path, 'wb') as local_file:
# The 'get' call is the async request
async for chunk in response.stream():
@@ -216,11 +237,11 @@ async def fetch_file(satellite:str=None, provider:str=None, path:str=None, size:
raise
finally:
- if progress:
- if down_task:progress.remove_task(down_task)
+ if progress and down_task is not None:
+ progress.remove_task(down_task)
-async def fetch_ntl(found_paths:dict[str, list]=None, satellite:str=None, dst_dir='/tmp', progress=None):
+async def fetch_ntl(found_paths:dict[str, list]=None, satellite:str=None, dst_dir:str=None, progress=None):
# Download logic (Surgical io to local SSD)
tasks = []
@@ -242,22 +263,136 @@ async def fetch_ntl(found_paths:dict[str, list]=None, satellite:str=None, dst_di
finally:
results = [t.result() for t in tasks]
-
return results
-
-
-
async def download(satellite:str=None, timestamp:str=None, source:str=None,
- products:Iterable[str]=PRODUCT_NAMES, dest_dir='/tmp', progress=None):
+ products:Iterable[str]=PRODUCT_NAMES, dst_dir:str=None, progress=None):
dt = datetime.strptime(timestamp, '%Y%m%d%H%M')
logger.info(f'Locating files for satellite {satellite} timestamp {timestamp} ')
found_files = await locate_file(satellite=satellite, dt=dt, source=source, products=products)
- return await fetch_ntl(found_paths=found_files, dst_dir=dest_dir, satellite=satellite, progress=progress)
+ return await fetch_ntl(found_paths=found_files, dst_dir=dst_dir, satellite=satellite, progress=progress)
def bytesto(bytes, to, bsize=1024):
a = {'k' : 1, 'm': 2, 'g' : 3, 't' : 4, 'p' : 5, 'e' : 6 }
r = float(bytes)
- return bytes / (bsize ** a[to])
\ No newline at end of file
+ return bytes / (bsize ** a[to])
+
+
+
+
+
+def create_area_from_geotransform(gt, array_shape):
+ """
+ Creates a Pyresample AreaDefinition exactly matching a GDAL array.
+
+ Args:
+ gt (tuple): GDAL GeoTransform (TopLeftX, PixelWidth, Rot, TopLeftY, Rot, PixelHeight)
+ array_shape (tuple): (height, width) of the target array
+ """
+ height, width = array_shape
+
+ # Calculate exact bounding box edges based on pixels
+ ll_x = gt[0]
+ ur_y = gt[3]
+ ur_x = gt[0] + (width * gt[1])
+ ll_y = gt[3] + (height * gt[5]) # gt[5] is usually negative
+
+ area_extent = [ll_x, ll_y, ur_x, ur_y]
+
+ # Build the EPSG:4326 definition
+ area_def = geometry.AreaDefinition(
+ area_id='gdal_matched_grid',
+ description='Grid mapped directly from Level-3 Baseline GeoTransform',
+ proj_id='EPSG:4326',
+ projection={'proj': 'longlat', 'datum': 'WGS84'},
+ width=width,
+ height=height,
+ area_extent=area_extent
+ )
+
+ return area_def
+
+
+
+
+
+def read_and_align_sdr(sdr_path, geo_path, target_area):
+ # 1. Calculate a slightly buffered crop box for Satpy
+ lon_min, lat_min, lon_max, lat_max = target_area.area_extent_ll
+ pad = 0.2 # Add a 0.2 degree buffer so EWA has data for edge pixels
+ crop_bbox = (lon_min - pad, lat_min - pad, lon_max + pad, lat_max + pad)
+
+ # 2. Load the scene
+ scn = Scene(filenames=[sdr_path, geo_path], reader='viirs_sdr')
+ scn.load(['DNB'])
+
+ # 3. Crop in memory BEFORE resampling (Massive speed/RAM boost)
+ cropped_scn = scn.crop(ll_bbox=crop_bbox)
+
+ # 4. Resample directly to the baseline's grid footprint
+ resampled_scn = cropped_scn.resample(
+ target_area,
+ resampler='ewa',
+ rows_per_scan=16,
+ fill_value=-999.0 # Explicitly manage fill values
+ )
+
+ dnb_array = resampled_scn['DNB'].values
+ dnb_array = np.where(dnb_array < -900, np.nan, dnb_array)
+
+ return dnb_array * 1e5 # Scale to match Black Marble nW/cm2/sr
+
+
+
+def read_and_align_sdr_and_cmask(sdr_path, geo_path, cmask_path, target_area):
+ """
+ Loads, crops, and resamples VIIRS SDR (Radiance) and EDR (Cloud Mask)
+ using the native fill values extracted dynamically from the file metadata.
+ """
+
+
+ # 2. Load the Scenes
+ sdr_scn = Scene(filenames=[sdr_path, geo_path], reader='viirs_sdr')
+ sdr_scn.load(['DNB'])
+
+ cmask_scn = Scene(filenames=[cmask_path], reader='viirs_edr')
+ cmask_scn.load(['CloudMask'])
+
+
+ # ---------------------------------------------------------
+ # 4. DYNAMIC FILL VALUE EXTRACTION
+ # Pull the exact _FillValue from the file attributes.
+ # We provide a safe fallback just in case the attribute is missing.
+ # ---------------------------------------------------------
+ dnb_fill = sdr_scn['DNB'].attrs.get('_FillValue', np.nan)
+
+ # EDR categorical data usually defaults to 255 or -127 for unsigned/signed bytes
+ cmask_fill = cmask_scn['CloudMask'].attrs.get('_FillValue', 255)
+
+ # 5. Resample SDR using the native fill value
+ resampled_sdr = sdr_scn.resample(
+ target_area,
+ resampler='ewa',
+ rows_per_scan=16,
+ fill_value=dnb_fill
+ )
+
+
+ # 6. Resample Cloud Mask
+ resampled_cmask = cmask_scn.resample(
+ target_area,
+ resampler='nearest',
+ radius_of_influence=1000,
+ fill_value=cmask_fill
+ )
+
+ # 7. Extract raw NumPy arrays and mask
+ dnb_array = resampled_sdr['DNB'].values
+
+
+ dnb_scaled = dnb_array * 1e5 # Scale to match Black Marble nW/cm2/sr
+ cmask_array = resampled_cmask['CloudMask'].values
+
+ return dnb_scaled, cmask_array
\ No newline at end of file
diff --git a/rapida/ntl/noaa/search.py b/rapida/ntl/noaa/search.py
index 2e1efc5..8e0640f 100644
--- a/rapida/ntl/noaa/search.py
+++ b/rapida/ntl/noaa/search.py
@@ -3,24 +3,23 @@
"""
import json
import os.path
-import asyncio
from pyorbital.orbital import Orbital
from datetime import datetime, timedelta, date, time as dtime
-from pathlib import Path
import math
-import httpx
from dataclasses import dataclass, asdict
from rich.progress import Progress
-import time
import logging
from typing import Iterable, Optional
+import obstore
from rapida.ntl.noaa.io import (
-find_ntl, public_url, parse_noaa_timestamp, locate_file
+find_ntl, public_url, parse_noaa_timestamp, VIIRS_STORES, SOURCE_NAMES, PRODUCTS
)
-
+import asyncio
+from spacetrack import SpaceTrackClient
from rapida.ntl.noaa.cmask import cloud_coverage_batch
from rapida.ntl.noaa.const import PRODUCTS_RE
from rapida.ntl import cache
+import numpy as np
logger = logging.getLogger(__name__)
@@ -71,7 +70,7 @@ def rank(self):
if str(self.cloud_cover).isnumeric():
clear_sky_score = 100 - self.cloud_cover
# 70% Weather, 30% Geometry
- return int((self.sat_rank * 0.3) + (clear_sky_score * 0.7))
+ return int((self.sat_rank * 0.4) + (clear_sky_score * 0.6))
return int(self.sat_rank)
@@ -79,7 +78,7 @@ def __hash__(self):
return hash(str(self))
def __str__(self):
- ddict = dict(satellite=self.sat, timestamp=self.timestamp,offset_km=self.offset,elevation=self.elevation, cloud_coverage=self.cloud_cover, rank=self.rank)
+ ddict = dict(satellite=self.sat, timestamp=self.timestamp,offset_km=self.offset,elevation=self.elevation, cloud_coverage=self.cloud_cover, rank=self.rank, bbox_perc_intersection=self.pint)
return json.dumps(ddict, separators=(',', ':'),)
def __repr__(self):
return f'{self.sat} granule {self.id} with sat rank {self.sat_rank:0d} and offset {self.offset} km from SSP featuring elevation of {self.elevation:.0f} degrees '
@@ -203,174 +202,231 @@ class VIIRSNavigator:
# THE OFFLINE MASTER SEEDS (Locked in April 2026)
# Drift is 'Seconds shifted per 24 hours'
#
- # the drifting was comuted by analyiz the tiomestamp of the first image produced by each satellite
+ # the drifting was comuted by analyiz the timestamp of the first image produced by each satellite
# ex for SNPP using rclone
# for i in $(seq 0 30); do T_DATE=$(date -d "2026-04-15 - $i days" +%Y/%m/%d); echo -n "$T_DATE | "; rclone lsf --s3-provider AWS --s3-region us-east-1 --s3-no-check-bucket ":s3:noaa-nesdis-snpp-pds/VIIRS-DNB-SDR/$T_DATE/" --include "*t00*.h5" -q | sort | head -n 1 | grep -o 't[0-9]\{7\}' | sed 's/t//'; done
SATELLITES = {'SNPP':'SUOMI NPP', 'N20':'NOAA 20', 'N21':'NOAA 21'}
- SAT_CONFIGS = {
- "N21": {"ref": date(2026, 4, 14), "phase": 67.2, "drift": -25.80},
- "N20": {"ref": date(2026, 4, 14), "phase": 68.0, "drift": -25.71},
- "SNPP": {"ref": date(2026, 4, 14), "phase": 68.4, "drift": -25.37}
- }
+
MIN_ELEVATION_ANGLE = 20
- def __init__(self, satellite=None, tle_file='/tmp/rapida.tle'):
+ def __init__(self, satellite=None):
self.satellite = satellite
- self.tle_file = self.get_tle(tle_file)
- self.orb = Orbital(satellite=self.satellite, tle_file=str(self.tle_file))
- self.cfg = self.SAT_CONFIGS[self.satellite]
-
-
-
- def fetch_tle(self):
+ def fetch_cached_tle(self, target_date: date=None, cache=cache ) -> str:
"""
- Surgically fetches VIIRS TLEs one-by-one to avoid query errors
- and IP bans. Merges them into a single in-memory string.
+ Fetches the TLE for a specific date, checking the shelve cache first.
+ If missing, hits Space-Track, caches it, and returns the formatted string.
"""
- # 37849: Suomi-NPP | 43013: NOAA-20 | 54234: NOAA-21
- targets = {
- "37849": "SNPP",
- "43013": "N20",
- "54234": "N21"
- }
-
- merged_tle = ""
-
- # Using the .org domain directly to avoid the 301 redirect penalty
- base_url = "https://celestrak.org/NORAD/elements/gp.php"
-
- # A professional User-Agent is your best shield against bans
- headers = {
- 'User-Agent': 'UNDP RAPIDA-Engine)',
- 'Accept': 'text/plain'
- }
-
- with Progress(disable=False, console=None, transient=True) as progress:
-
- total_task = progress.add_task("[cyan]Initializing TLE io...", total=len(targets))
-
- with httpx.Client(timeout=15.0, follow_redirects=True) as client:
- for catnr, name in targets.items():
- params = {
- 'CATNR': catnr,
- 'FORMAT': 'TLE'
- }
-
- try:
- response = client.get(base_url, params=params, headers=headers)
-
- # RULE: If we hit an error, STOP. Don't hammer the server.
- if response.status_code != 200:
- progress.console.log(f"🛑 Error {response.status_code} for {name}. Aborting to avoid IP ban.")
- break
-
- # Validate that we actually got a TLE (should start with name or '1 ')
- data = response.text.strip()
- if "1 " in data:
- # Split the response into lines
- lines = [l.strip() for l in response.text.strip().splitlines() if l.strip()]
-
- # CelesTrak usually returns 3 lines (Name, L1, L2)
- # or 2 lines (L1, L2) if CATNR is used.
- # We only care about the last two lines (the TLE data)
- if len(lines) >= 2 and lines[-2].startswith("1 "):
- tle_l1 = lines[-2]
- tle_l2 = lines[-1]
-
- # We MANUALLY prepend our clean name
- merged_tle += f"{name}\n{tle_l1}\n{tle_l2}\n"
- progress.advance(total_task)
- progress.console.log(f"[green]✅ Successfully fetched TLE for {name}")
-
- else:
- progress.console.log(f"[yellow]⚠️ Received invalid data for {name}.")
-
- except Exception as e:
- progress.console.log(f"[red]❌ Network error on {name}: {e}")
- break
+ # Deterministic cache key
+ key = f"TLE_{self.satellite}_{target_date.strftime('%Y%m%d')}"
+ tle_data = cache.fetch(key=key)
+ if not tle_data:
+ user = os.getenv('SPACETRACK_USER')
+ if user is None:
+ raise EnvironmentError("SPACETRACK_USER is missing from environment variables.")
+
+ passwd = os.getenv('SPACETRACK_PASSWORD')
+ if passwd is None:
+ raise EnvironmentError("SPACETRACK_PASSWORD is missing from environment variables.")
+ # 2. Network Fetch (Space-Track)
+ st = SpaceTrackClient(
+ identity=user,
+ password=passwd
+ )
- # THE "GOOD CITIZEN" DELAY:
- # CelesTrak specifically asks for breaks between requests.
- # Advance the bar and update description for the "Good Citizen" sleep
+ cat_ids = {"SNPP": "37849", "N20": "43013", "N21": "54234"}
+ start_date = (target_date - timedelta(days=3)).strftime('%Y-%m-%d')
+ end_date = (target_date + timedelta(days=1)).strftime('%Y-%m-%d')
- if catnr != list(targets.keys())[-1]: # Don't sleep after the last target
- progress.update(total_task,
- description=f"[dim white]Respecting CelesTrak rate limits (2s)...")
- time.sleep(2.0)
+ tle_data = st.gp_history(
+ norad_cat_id=cat_ids[self.satellite],
+ epoch=f"{start_date}--{end_date}",
+ orderby='EPOCH desc',
+ limit=1,
+ format='tle'
+ )
- if not merged_tle:
- raise RuntimeError("🚨 Failed to io any TLE data. Probably IP-blocked. Should reset in two ours.\
- Alternatively download manually 'https://celestrak.org/NORAD/elements/gp.php?GROUP=weather' to /tmp/rapida_tle.txt")
+ if not tle_data:
+ raise ValueError(f"No TLE found for {self.satellite} on {target_date}")
+ cache.store(key=key, value=tle_data)
- return merged_tle
- def get_tle(self, tle_file ):
- # Pathlib handles the '/' vs '\' slash drama automatically
- cache_file = Path(tle_file)
+ return tle_data
- # 1. Does it exist and is it fresh? (7200 seconds = 2 hours)
- if cache_file.exists() and cache_file.stat().st_size > 0:
- age = time.time() - cache_file.stat().st_mtime
- if age < 12*3600:
- return cache_file
+ # def fetch_tle(self):
+ # """
+ # Surgically fetches VIIRS TLEs one-by-one to avoid query errors
+ # and IP bans. Merges them into a single in-memory string.
+ # """
+ # # 37849: Suomi-NPP | 43013: NOAA-20 | 54234: NOAA-21
+ # targets = {
+ # "37849": "SNPP",
+ # "43013": "N20",
+ # "54234": "N21"
+ # }
+ #
+ # merged_tle = ""
+ #
+ # # Using the .org domain directly to avoid the 301 redirect penalty
+ # base_url = "https://celestrak.org/NORAD/elements/gp.php"
+ #
+ # # A professional User-Agent is your best shield against bans
+ # headers = {
+ # 'User-Agent': 'UNDP RAPIDA-Engine)',
+ # 'Accept': 'text/plain'
+ # }
+ #
+ # with Progress(disable=False, console=None, transient=True) as progress:
+ #
+ # total_task = progress.add_task("[cyan]Initializing TLE io...", total=len(targets))
+ #
+ # with httpx.Client(timeout=15.0, follow_redirects=True) as client:
+ # for catnr, name in targets.items():
+ # params = {
+ # 'CATNR': catnr,
+ # 'FORMAT': 'TLE'
+ # }
+ #
+ # try:
+ # response = client.get(base_url, params=params, headers=headers)
+ #
+ # # RULE: If we hit an error, STOP. Don't hammer the server.
+ # if response.status_code != 200:
+ # progress.console.log(f"🛑 Error {response.status_code} for {name}. Aborting to avoid IP ban.")
+ # break
+ #
+ # # Validate that we actually got a TLE (should start with name or '1 ')
+ # data = response.text.strip()
+ # if "1 " in data:
+ # # Split the response into lines
+ # lines = [l.strip() for l in response.text.strip().splitlines() if l.strip()]
+ #
+ # # CelesTrak usually returns 3 lines (Name, L1, L2)
+ # # or 2 lines (L1, L2) if CATNR is used.
+ # # We only care about the last two lines (the TLE data)
+ # if len(lines) >= 2 and lines[-2].startswith("1 "):
+ # tle_l1 = lines[-2]
+ # tle_l2 = lines[-1]
+ #
+ # # We MANUALLY prepend our clean name
+ # merged_tle += f"{name}\n{tle_l1}\n{tle_l2}\n"
+ # progress.advance(total_task)
+ # progress.console.log(f"[green]✅ Successfully fetched TLE for {name}")
+ #
+ # else:
+ # progress.console.log(f"[yellow]⚠️ Received invalid data for {name}.")
+ #
+ # except Exception as e:
+ # progress.console.log(f"[red]❌ Network error on {name}: {e}")
+ # break
+ #
+ # # THE "GOOD CITIZEN" DELAY:
+ # # CelesTrak specifically asks for breaks between requests.
+ # # Advance the bar and update description for the "Good Citizen" sleep
+ #
+ # if catnr != list(targets.keys())[-1]: # Don't sleep after the last target
+ # progress.update(total_task,
+ # description=f"[dim white]Respecting CelesTrak rate limits (2s)...")
+ # time.sleep(2.0)
+ #
+ # if not merged_tle:
+ # raise RuntimeError("🚨 Failed to io any TLE data. Probably IP-blocked. Should reset in two ours.\
+ # Alternatively download manually 'https://celestrak.org/NORAD/elements/gp.php?GROUP=weather' to /tmp/rapida_tle.txt")
+ #
+ # return merged_tle
+ #
+ # def get_tle(self, tle_file ):
+ # # Pathlib handles the '/' vs '\' slash drama automatically
+ # cache_file = Path(tle_file)
+ #
+ # # 1. Does it exist and is it fresh? (7200 seconds = 2 hours)
+ # if cache_file.exists() and cache_file.stat().st_size > 0:
+ # age = time.time() - cache_file.stat().st_mtime
+ # if age < 12*3600:
+ # return cache_file
+ #
+ # # 2. If not, io and save (This only happens once every 2 hours)
+ # with open(cache_file, 'wt+') as tfile:
+ # tle_content = self.fetch_tle()
+ # tfile.write(tle_content)
+ # return cache_file
+ #
+ #
+ #
+ # def get_phase_for_date(self, target_date):
+ # """Calculates exact phase using the continuous spacecraft clock and physical drift."""
+ # if isinstance(target_date, datetime):
+ # target_date = target_date.date()
+ #
+ # target_midnight = datetime.combine(target_date, dtime(0, 0, 0))
+ # ref_midnight = datetime.combine(self.cfg["ref"], dtime(0, 0, 0))
+ # delta_seconds = (target_midnight - ref_midnight).total_seconds()
+ #
+ # # FIXED MATH: Apply the daily drift rate to the exact seconds elapsed
+ # drift_per_second = self.cfg["drift"] / 86400.0
+ # accumulated_drift = delta_seconds * drift_per_second
+ #
+ # # Add the accumulated drift, NOT the raw delta_seconds
+ # predicted = (self.cfg["phase"] + accumulated_drift) % self.GRANULE_DUR
+ #
+ # return predicted
- # 2. If not, io and save (This only happens once every 2 hours)
- with open(cache_file, 'wt+') as tfile:
- tle_content = self.fetch_tle()
- tfile.write(tle_content)
- return cache_file
- def get_phase_for_date_1(self, target_date):
- """Calculates the 100% offline phase for any day."""
- days_delta = (target_date.date() - self.cfg["ref"]).days
+ def get_orbital(self, target_date:date=None):
+ tle_text = self.fetch_cached_tle(target_date=target_date)
- # Predicted Phase = Initial + (Drift * Days)
- # Modulo solves the 'Midnight Hiccup' wrap-around automatically
- predicted = (self.cfg["phase"] + (days_delta * self.cfg["drift"])) % self.GRANULE_DUR
- return predicted
+ l1, l2 = (l.strip() for l in tle_text.strip().splitlines() if l.strip())
- def get_phase_for_date_2(self, target_date):
- """Calculates exact phase using the continuous spacecraft clock."""
+ return Orbital(satellite=self.satellite, line1=l1, line2=l2)
- # 1. Handle both 'date' and 'datetime' inputs safely
- if isinstance(target_date, datetime):
- target_date = target_date.date()
- # 2. Anchor both dates to Midnight UTC
- target_midnight = datetime.combine(target_date, dtime(0, 0, 0))
- ref_midnight = datetime.combine(self.cfg["ref"], dtime(0, 0, 0))
- # 3. Get exact elapsed seconds (can be negative if target is before ref)
- delta_seconds = (target_midnight - ref_midnight).total_seconds()
+ def get_phase_for_date_new(self, target_date:datetime.date=None):
+ orb = self.get_orbital(target_date=target_date)
+ t_epoch = orb.orbit_elements.epoch
+ # FIXED MATH: Force pyorbital's numpy.datetime64 into a standard Python datetime
+ if isinstance(t_epoch, np.datetime64):
+ # .item() cleanly converts a scalar numpy datetime back to standard python datetime
+ t_epoch = t_epoch.astype('datetime64[us]').item()
+ stores = VIIRS_STORES[self.satellite]
+ first_file_path = None
+ for source in SOURCE_NAMES:
- # 4. Predict phase using exact modulo arithmetic
- # Python's modulo perfectly handles negative time shifts
- predicted = (self.cfg["phase"] + delta_seconds) % self.GRANULE_DUR
+ store = stores[source]
+ product = PRODUCTS['CM']
+ date_path = target_date.strftime('/%Y/%m/%d/')
+ prefix = f"{product}{date_path}"
+ stream = obstore.list(store, prefix=prefix)
- return predicted
- def get_phase_for_date(self, target_date):
- """Calculates exact phase using the continuous spacecraft clock and physical drift."""
- if isinstance(target_date, datetime):
- target_date = target_date.date()
+ for chunk in stream:
+ for meta in chunk:
+ if meta["path"].endswith('.nc'):
+ first_file_path = meta["path"]
+ break
+ if first_file_path:
+ break
+ if not first_file_path: # move to another source
- target_midnight = datetime.combine(target_date, dtime(0, 0, 0))
- ref_midnight = datetime.combine(self.cfg["ref"], dtime(0, 0, 0))
- delta_seconds = (target_midnight - ref_midnight).total_seconds()
+ continue
+ if first_file_path is None:
- # FIXED MATH: Apply the daily drift rate to the exact seconds elapsed
- drift_per_second = self.cfg["drift"] / 86400.0
- accumulated_drift = delta_seconds * drift_per_second
+ logger.info(f'No VIIRS operational data was found for {self.satellite} {target_date.date()} in {" or ".join(SOURCE_NAMES)}. '
+ f'Please check manually at https://noaa-nesdis-{self.satellite.lower()}-pds.s3.amazonaws.com/index.html#VIIRS-JRR-CloudMask/ ')
+ return
+ _, filename = os.path.split(first_file_path)
+ m = PRODUCTS_RE['CM'].match(filename)
+ if m:
+ start_time = parse_noaa_timestamp(m.groupdict()['start'])
+ # 5. Calculate physical phase (Delta % Granule Duration)
+ delta = (start_time - t_epoch).total_seconds()
+ return delta % self.GRANULE_DUR
- # Add the accumulated drift, NOT the raw delta_seconds
- predicted = (self.cfg["phase"] + accumulated_drift) % self.GRANULE_DUR
- return predicted
def decompose_bbox(self, bbox:Iterable[float]=None):
@@ -385,9 +441,30 @@ def decompose_bbox(self, bbox:Iterable[float]=None):
return midlon, midlat, maxlat
+ # def pass2granule(self, p:DescendingPass=None, midlon:float=None, midlat:float=None, elevation:float=None ):
+ # orb = self.get_orbital()
+ # phase = self.get_phase_for_date(p.target_date)
+ # sat_lon, _, _ = self.orb.get_lonlatalt(p.max_elev_time)
+ # deg_offset = abs(midlon - sat_lon)
+ # # Physical distance in km at this latitude
+ # offset_km = int(deg_offset * 111.32 * math.cos(math.radians(midlat)))
+ # # Anchor to Midnight UTC of the target day
+ # t_midnight = datetime.combine(p.target_date.date(), dtime(0, 0, 0))
+ # delta_seconds = (p.max_elev_time - t_midnight).total_seconds()
+ #
+ # # 2. Pulse-Sync Math
+ # pulse_index = math.floor((delta_seconds - phase) / self.GRANULE_DUR)
+ # start_time = t_midnight + timedelta(seconds=(pulse_index * self.GRANULE_DUR) + phase)
+ #
+ #
+ # return Granule(sat=self.satellite,start_time=start_time,offset=offset_km, elevation=elevation)
+
def pass2granule(self, p:DescendingPass=None, midlon:float=None, midlat:float=None, elevation:float=None ):
- phase = self.get_phase_for_date(p.target_date)
- sat_lon, _, _ = self.orb.get_lonlatalt(p.max_elev_time)
+ phase = self.get_phase_for_date_new(target_date=p.target_date)
+ if phase is None:
+ return
+ orb = self.get_orbital(p.target_date)
+ sat_lon, _, _ = orb.get_lonlatalt(p.max_elev_time)
deg_offset = abs(midlon - sat_lon)
# Physical distance in km at this latitude
offset_km = int(deg_offset * 111.32 * math.cos(math.radians(midlat)))
@@ -402,57 +479,61 @@ def pass2granule(self, p:DescendingPass=None, midlon:float=None, midlat:float=No
return Granule(sat=self.satellite,start_time=start_time,offset=offset_km, elevation=elevation)
- def night_passes(self, bbox:Iterable[float]=None, target_date:date=None):
-
+ def night_passes(self, bbox:Iterable[float]=None, nominal_date:date=None):
+ orb = self.get_orbital(target_date=nominal_date)
midlon, midlat, northlat = self.decompose_bbox(bbox=bbox)
# 1. NIGHT DURATION (Use Mid-Lat for 'Average' Night)
- doy = target_date.timetuple().tm_yday
+ doy = nominal_date.timetuple().tm_yday
declination = 0.409 * math.sin(2 * math.pi * (doy - 80) / 365)
lat_rad = math.radians(midlat)
cos_h = -math.tan(lat_rad) * math.tan(declination)
night_hrs = int(round(24 - (2 * math.degrees(math.acos(max(-1.0, min(1.0, cos_h)))) / 15)))
-
+ search_duration_hrs = max(10, night_hrs)
# 2. THE ANCHOR (01:30 AM Local -> UTC)
- utc_anchor = datetime.combine(target_date, dtime(1, 30)) - timedelta(hours=midlon / 15.0)
+ utc_anchor = datetime.combine(nominal_date, dtime(1, 30)) - timedelta(hours=midlon / 15.0)
# 3. THE TRIGGER (Use North-Lat to find when the satellite ENTERS the box)
- search_start = utc_anchor - timedelta(hours=night_hrs / 2)
- night_passes = self.orb.get_next_passes(search_start, night_hrs, midlon, midlat, 0) # northlat???
- logger.debug(f'{self.satellite} passes {len(night_passes)} time(s) over {list(bbox)} on the night of {target_date:%y-%m-%d}')
+ search_start = utc_anchor - timedelta(hours=search_duration_hrs / 2)
+ night_passes = orb.get_next_passes(search_start, search_duration_hrs, midlon, midlat, 0) # northlat???
+ logger.debug(f'{self.satellite} passes {len(night_passes)} time(s) over {list(bbox)} on the night of {nominal_date:%y-%m-%d}')
passes = []
for _pass_ in night_passes:
rise_time, fall_time, max_elev_time = _pass_
# Direction Check
- pos_start = self.orb.get_lonlatalt(rise_time)
- pos_end = self.orb.get_lonlatalt(fall_time)
+ pos_start = orb.get_lonlatalt(rise_time)
+ pos_end = orb.get_lonlatalt(fall_time)
if not pos_end[1] < pos_start[1]: # Descending
logger.debug(f'Skipping ascending pass {_pass_}')
continue
p = DescendingPass(sat=self.satellite, rise_time=rise_time, fall_time=fall_time,
- max_elev_time=max_elev_time, target_date=target_date)
+ max_elev_time=max_elev_time, target_date=nominal_date)
passes.append(p)
return passes
- async def night_granules_async(self, bbox:Iterable[float]=None, target_date:date=None, cmask=False, progress=None):
- midlon, midlat, northlat = self.decompose_bbox(bbox=bbox)
- passes = self.night_passes(target_date=target_date, bbox=bbox)
+ async def night_granules_async(self, bbox:Iterable[float]=None, nominal_date:date=None, cmask=False, progress=None):
+ midlon, midlat, northlat = self.decompose_bbox(bbox=bbox)
+ passes = self.night_passes(nominal_date=nominal_date, bbox=bbox)
+ orb = self.get_orbital(target_date=nominal_date)
selected_granules = {}
granules = []
+
for p in passes:
- look = self.orb.get_observer_look(p.max_elev_time, midlon, midlat, 0)
+ look = orb.get_observer_look(p.max_elev_time, midlon, midlat, 0)
elevation = look[1]
if elevation < self.MIN_ELEVATION_ANGLE:
logger.info(f'Skipping {p} because of low elevation angle {elevation:0f}')
continue
- granule = self.pass2granule(p=p,midlon=midlon, midlat=midlat, elevation=elevation, )
- granules.append(granule)
+ granule = self.pass2granule(p=p,midlon=midlon, midlat=midlat, elevation=elevation,)
+ if granule:
+ granules.append(granule) # return noe in case no data is available with hyper-scalers
+
geom_granules = await granules2files(
@@ -489,106 +570,24 @@ async def night_granules_async(self, bbox:Iterable[float]=None, target_date:date
return selected_granules
- def night_granules(self, bbox:Iterable[float]=None, target_date:date=None, cmask:bool=False, progress=None ):
- midlon, midlat, northlat = self.decompose_bbox(bbox=bbox)
- passes = self.night_passes(target_date=target_date, bbox=bbox)
-
- selected_granules = {}
- granules = []
- for p in passes:
- look = self.orb.get_observer_look(p.max_elev_time, midlon, midlat, 0)
- elevation = look[1]
- if elevation < self.MIN_ELEVATION_ANGLE:
- logger.debug(f'Skipping {p} because of low elevation angle {elevation:0f}')
- continue
- granule = self.pass2granule(p=p,midlon=midlon, midlat=midlat, elevation=elevation, )
-
- granules.append(granule)
-
-
- geom_granules = asyncio.run(granules2files(
- granules=granules,satellite=self.satellite, bbox=bbox, progress=progress
- ))
-
- for current_granule, found in geom_granules.items():
- if not found:
- continue
- # Safely get the first source/entry
- (source, entries), = found.items()
- file_path, _ = entries[0]
-
- _, file_name = os.path.split(file_path)
- if f's{current_granule.timestamp}' not in file_name:
- m = PRODUCTS_RE['CM'].match(file_name)
- if m:
- start_time = parse_noaa_timestamp(m.groupdict()['start'])
- old_timestamp = current_granule.timestamp
- current_granule.start_time = start_time
- logger.debug(f'Replacing granule {old_timestamp} with {current_granule.timestamp}')
-
- if cmask:
- # Use the unique URL as the key (Always unique)
- url = public_url(file_path=file_path, satellite=self.satellite, source=source)
- selected_granules[url] = current_granule
- else:
- # Use the unique file_path as the key to prevent SNPP/N20/N21 overwrites
- selected_granules[file_path] = current_granule
-
-
- return selected_granules
-
-
-
-def search_granules(satellites:Optional[Iterable[str]]=None,
- target_date:date=None, bbox:Iterable[float] = None,
- cmask:bool=False, progress=None):
-
- satellite_names = list(VIIRSNavigator.SAT_CONFIGS.keys())
- assert isinstance(target_date, date), f'invalid target date {target_date}'
- satellites = satellites or satellite_names
- selected_granules = []
- found_granules = {}
- for sat in satellites:
- logger.debug(f'Locating imagery (data granules) for {sat} satellite')
- nav = VIIRSNavigator(satellite=sat)
- sat_granules = nav.night_granules(bbox=bbox, target_date=target_date, cmask=cmask, progress=progress)
- found_granules.update(sat_granules)
- if cmask:
- cloud_coverage_results = cloud_coverage_batch(urls=list(found_granules.keys()), bbox=bbox, progress=progress)
-
- for cm_url, g in found_granules.items():
- cloud_cover = cloud_coverage_results[cm_url]
- if cloud_cover is None:cloud_cover = 'Not Available'
- if isinstance(cloud_cover, Exception):
- continue
- g.cloud_cover = cloud_cover
- g.url = os.path.split(cm_url)[-1]
- selected_granules.append(g)
- selected_granules.sort(key=lambda g: g.rank, reverse=True)
- return selected_granules
- else:
- selected_granules = list(found_granules.values())
- selected_granules.sort(key=lambda g: g.rank, reverse=True)
- return selected_granules
-
async def async_search_granules(
- satellites:Optional[Iterable[str]]=None, target_date:date=None, bbox:Iterable[float] = None,
- cmask=False, progress=None
+ satellites:Optional[Iterable[str]]=None, nominal_date:date=None, bbox:Iterable[float] = None,
+ cmask=False, progress=None, push_to_cache:bool=False,
):
"""
:param satellites:
- :param target_date:
+ :param nominal_date:
:param bbox:
:param cmask:
:param progress:
:return:
"""
- satellite_names = list(VIIRSNavigator.SAT_CONFIGS.keys())
- assert isinstance(target_date, date), f'invalid target date {target_date}'
+ satellite_names = list(VIIRSNavigator.SATELLITES.keys())
+ assert isinstance(nominal_date, date), f'invalid target date {nominal_date}'
satellites = satellites or satellite_names
selected_granules = []
found_granules = {}
@@ -600,7 +599,7 @@ async def async_search_granules(
for sat in satellites:
nav = VIIRSNavigator(satellite=sat)
tasks.append(tg.create_task(
- nav.night_granules_async(bbox=bbox, target_date=target_date, cmask=cmask,
+ nav.night_granules_async(bbox=bbox, nominal_date=nominal_date, cmask=cmask,
progress=progress)
))
@@ -614,6 +613,9 @@ async def async_search_granules(
if progress and progress_task is not None:
progress.remove_task(progress_task)
+
+
+
if cmask:
cloud_coverage_results = cloud_coverage_batch(urls=list(found_granules.keys()), bbox=bbox, progress=progress)
for cm_url, g in found_granules.items():
@@ -622,7 +624,7 @@ async def async_search_granules(
if isinstance(cloud_cover, Exception):
continue
g.cloud_cover = cloud_cover
- g.url = os.path.split(cm_url)[-1]
+ g.url = cm_url
selected_granules.append(g)
selected_granules.sort(key=lambda g: g.rank, reverse=True)
@@ -633,47 +635,71 @@ async def async_search_granules(
return selected_granules
-# if __name__ == '__main__':
-# import asyncio
-# logging.basicConfig()
-# logger = logging.getLogger()
-#
-# logger.setLevel(logging.INFO)
-# logging.getLogger('httpx').setLevel(logging.WARNING)
-# logger.name = 'ntlcli'
-#
-# # --- Usage Example ---
-# my_lat, my_lon = 49.75, 16.5
-# target_date = datetime(2026, 4, 2)
-# czbbox = 14.0, 48.5, 19.0, 51.0
-#
-# bboxes = [
-# [51.3337,35.6443,51.4443,35.7341],
-# [48.2393,30.2947,48.3433,30.3845],
-# [48.1104,30.3926,48.2146,30.4824],
-# [51.6147,32.6097,51.7213,32.6995],
-# [48.3468,32.3384,48.4532,32.4282],
-# [48.618,31.2734,48.7232,31.3632],
-# [46.2371,38.0324,46.3513,38.1222],
-# [47.0106,34.2693,47.1194,34.3591],
-# [52.532,29.5469,52.6354,29.6367],
-# [50.8218,34.5952,50.931,34.685]
-# ]
-# names = 'Tehran,Abadan,Khorramshahr,Isfahan,Dezful,Ahvaz,Tabriz,Kermanshah,Shiraz,Qom'
-# names= names.split(',')
-# data = list(zip(names, bboxes))
-#
-# for sat in VIIRSNavigator.SATELLITES:
-# n = VIIRSNavigator(satellite=sat)
-# # passes = n.night_passes(bbox=bboxes[-1], target_date=target_date, elevation_filter=False)
-# # for p, g in passes.items():
-# # print(p, g)
-#
-#
-# bp, bg = n.best_pass(bbox=bboxes[-1], target_date=target_date, avoid_clouds=False)
-# print(f'Best pass: {bp} {bg}')
-#
-#
+if __name__ == '__main__':
+ import asyncio
+ from datetime import datetime
+ # import os
+ # os.environ['UV_ENV_FILE'] = '/home/work/py/geo-cb-surge/.env'
+ # from ntl.io.operational import download
+ # from ntl.search.cmr import fetch
+ # from ntl.utils import vis
+ from rich.logging import RichHandler
+
+ logging.basicConfig(
+ level=logging.DEBUG, # Or whatever level you use
+ format="%(message)s", # RichHandler handles the timestamps and formatting natively
+ datefmt="[%X]",
+ handlers=[RichHandler(rich_tracebacks=True, markup=True)]
+ )
+ logger = logging.getLogger()
+ logger.setLevel(logging.INFO)
+ logging.getLogger('httpx').setLevel(logging.WARNING)
+ logger.name = 'ntloper'
+ events = {
+ 'Tehran': ('28-02-2026', (50.8, 35.3, 51.9, 36)), # bombing
+ 'Abuja': ('23-01-2026', (7, 8.5, 7.8, 9.4)), # grid failure
+ 'Dominican Rep': ('23-02-2026', (-72.00, 17.50, -68.30, 20.00)), # national grid failure
+ 'Kharkiv/Dnipro': ('26-01-2026', (34.4, 48.2, 38.3, 50.6)), # Kharkiv/Dnipro grid attacks
+ 'Porto Rico': ('26-09-2017', (-67.8, 17.6, -65.2, 18.6)),
+ 'Bahia Blanca': ('18-12-2023', (-62.4, -38.8, -62.2, -38.6))
+ }
+
+ site = 'Abuja'
+ datestr, bbox = events[site]
+ event_date = datetime.strptime(datestr, '%d-%m-%Y')
+ target_date = event_date + timedelta(days=1)
+
+ # resolution = 750
+ #
+ # dst_dir = '/tmp'
+ # files = [e for e in os.scandir(dst_dir) if e.name.endswith('.h5') or e.name.endswith('.nc')]
+ # print(files)
+ # cmask_file_path = None
+ # for e in files:
+ # if e.name.startswith('SVDNB'): sdr_file_path = e.path
+ # if e.name.startswith('GDNBO'): geolocation_file_path = e.path
+ # if e.name.startswith('JRR-Cloud'): cmask_file_path = e.path
+ # if '46A3' in e.name: baseline_file_path = e.path
+ # if not cmask_file_path:
+ with Progress(disable=False, transient=False) as progress:
+
+ for s in VIIRSNavigator.SATELLITES:
+ n = VIIRSNavigator(satellite=s)
+ print(n)
+ phase = n.get_phase_for_date(target_date)
+ new_phase = n.get_phase_for_date_new(target_date=target_date)
+ print(s, phase, new_phase)
+ break
+
+ # granules = asyncio.run(async_search_granules( satellites=['SNPP'],
+ # nominal_date=target_date, bbox=bbox, cmask=True, progress=progress
+ # ))
+
+ # for g in granules:
+ # print(g)
+
+
+
diff --git a/rapida/ntl/outage.py b/rapida/ntl/outage.py
new file mode 100644
index 0000000..09d332f
--- /dev/null
+++ b/rapida/ntl/outage.py
@@ -0,0 +1,168 @@
+
+from datetime import datetime, timedelta
+import numbers
+import logging
+from rich.progress import Progress
+from rapida.ntl.utils import write_outage_tif
+from rapida.ntl.fetch import fetch
+from rapida.ntl.nasa.io import extract_bb
+from rapida.ntl.noaa.io import create_area_from_geotransform, read_and_align_sdr_and_cmask
+from rapida.ntl.nasa import const as nasa_const
+from rapida.ntl.noaa import const as noaa_const
+import numpy as np
+import os
+from rapida.ntl import utils
+from rapida.admin.util import bbox_to_geojson_polygon
+logger = logging.getLogger('rapida')
+
+
+
+async def detect_outage(
+ bbox: tuple[numbers.Number] = None, nominal_date: datetime = None, deliverable: str = None,
+ dst_dir: str = None, mask_clouds:bool = True, percentage_drop:int = None,
+ display: bool = False, progress: Progress = None):
+
+ logger.info(f'Fetching best imagery for {deliverable} {bbox}-{nominal_date} ')
+ # with open(os.path.join('/tmp', 'bbox.geojson'), "w") as ff:
+ # ff.write(bbox_to_geojson_polygon(*bbox, as_string=True))
+ #fetch daily data, source independent
+ # --- 2. FETCH DAILY TARGET DATA ---
+ daily_results = await fetch(bbox=bbox, nominal_date=nominal_date, deliverable=deliverable,
+ progress=progress, dst_dir=dst_dir)
+ if not daily_results:
+ logger.info(f'No imagery was found for {nominal_date:"%Y%m%d"} over {bbox} {deliverable.split("_")[0]}')
+ logger.info(f'Consider adjusting source, date or the bounding box')
+ return
+ for timestamp, product_files in daily_results.items():
+ print(timestamp, product_files)
+
+ arrays = {}
+ # --- 1. FETCH & PROCESS MONTHLY BASELINE ---
+ monthly_nominal_date = nominal_date - timedelta(weeks=6) # the search can handle but its better ot explicit
+ monthly_results = await fetch(bbox=bbox, nominal_date=monthly_nominal_date,
+ progress=progress, deliverable='baseline', dst_dir=dst_dir)
+
+ monthly_timestamp, monthly_files = next(iter(monthly_results.items()))
+ monthly_product, monthly_image_files = next(iter(monthly_files.items()))
+
+ monthly_data, gt = extract_bb(image_files=monthly_image_files, sds_name=nasa_const.SUB_DATASETS['A3'],
+ bbox=bbox, progress=progress, return_gt=True)
+
+ monthly_data_label = f'{monthly_product}_{monthly_timestamp}'
+ monthly_positive = monthly_data >= 0
+ log_monthly_data = np.zeros_like(monthly_data)
+ log_monthly_data[monthly_positive] = np.log1p(monthly_data[monthly_positive])
+ arrays[monthly_data_label] = log_monthly_data
+ # land water mask 0 = Land & Desert 1 = Land no Desert 2 = Inland Water 3 = Sea Water 5 = Coastal
+ # Create a mask for all land pixels including the coastal
+ land_water_background = extract_bb(image_files=monthly_image_files, sds_name='Land_Water_Mask', bbox=bbox, progress=progress)
+ analysis_mask = (land_water_background == 2) | (land_water_background == 3)
+ analysis_mask |= ~monthly_positive
+
+ if 'NOAA' in deliverable:
+ target_area = create_area_from_geotransform(gt, monthly_data.shape)
+
+ # 1. Initialize master COMPOSITE arrays
+ # Use NaNs so unobserved pixels remain NaN
+ log_diff = np.full_like(monthly_data, np.nan)
+ zscore = np.full_like(monthly_data, np.nan)
+ outage = np.zeros_like(monthly_data, dtype=bool)
+
+ # 2. Master tracking mask (True if a pixel gets AT LEAST ONE clear observation)
+ valid_mask = np.zeros_like(analysis_mask, dtype=bool)
+
+ for timestamp, product_files in daily_results.items():
+
+ daily_data, cloud_mask = read_and_align_sdr_and_cmask(
+ sdr_path=product_files[noaa_const.SDR], geo_path=product_files[noaa_const.GEO],
+ cmask_path=product_files[noaa_const.CM], target_area=target_area
+ )
+
+ # 3. Create a STRICTLY DAILY invalid mask
+ # It inherits the base analysis_mask (water/bg), but does NOT inherit yesterday's clouds
+ daily_invalid_mask = np.isnan(daily_data) | analysis_mask
+
+ if mask_clouds is True:
+ is_cloudy = cloud_mask > 1
+ daily_invalid_mask |= is_cloudy
+
+ log_daily_data = np.zeros_like(daily_data)
+ log_daily_data[~daily_invalid_mask] = np.log1p(daily_data[~daily_invalid_mask])
+
+ # Save individual daily raw data if needed
+ daily_data_label = f'{noaa_const.SDR}_{timestamp}'
+ arrays[daily_data_label] = log_daily_data
+
+ # Run the NTL statistics engine on today's clear pixels
+ granule_logdiff, granule_zscore, granule_outage = utils.logdiff_outage(
+ log_monthly_data=log_monthly_data, log_daily_data=log_daily_data,
+ analysis_mask=daily_invalid_mask, percentage_drop=percentage_drop
+ )
+
+ # 4. Update the Master Composites
+ # We define what was valid TODAY
+ daily_valid = ~daily_invalid_mask
+
+ # Isolate pixels that are valid in THIS granule AND haven't been mapped yet today
+ is_first_time = daily_valid & ~valid_mask
+
+ # Lock in the data ONLY for these first-time pixels
+ log_diff[is_first_time] = granule_logdiff[is_first_time]
+ zscore[is_first_time] = granule_zscore[is_first_time]
+ outage[is_first_time] = granule_outage[is_first_time]
+
+ # Update our tracking mask to protect these pixels from future overlaps
+ valid_mask |= daily_valid
+
+ # Update our tracking mask to remember we saw these pixels
+ valid_mask |= daily_valid
+
+ ts = f'{nominal_date:%Y%m%d}'
+
+ # 5. Save the final merged dataset outside the loop
+ # Invert the valid mask so True = "We never saw this pixel clearly"
+ arrays[f'{ts}_MASK'] = ~valid_mask
+ arrays[f'{ts}_LOGDIFF'] = log_diff
+ arrays[f'{ts}_ZSCORE'] = zscore
+ arrays[f'{ts}_OUTAGE'] = outage
+
+ elif 'NASA' in deliverable:
+ for timestamp, product_files in daily_results.items():
+ for product, local_image_files in product_files.items():
+ level = product.split('_')[0][-2:] if 'NRT' in deliverable else product[-2:]
+ sub_dataset_name = nasa_const.SUB_DATASETS[level]
+ daily_data = extract_bb(image_files=local_image_files, bbox=bbox,sds_name=sub_dataset_name,progress=progress)
+ positive = daily_data>=0
+ log_daily_data = np.zeros_like(daily_data)
+ log_daily_data[positive] = np.log1p(daily_data[positive])
+ log_daily_data = np.log1p(daily_data)
+
+ daily_data_label = f'{product}_{timestamp}'
+ arrays[daily_data_label] = log_daily_data
+
+ if mask_clouds:
+ qf_array = extract_bb(image_files=local_image_files, sds_name='QF_Cloud_Mask',
+ bbox=bbox, progress=progress).astype('u2')
+ cloud_confidence = (qf_array >> 6) & 0b11
+ is_cloudy = cloud_confidence == 3
+ arrays['CLOUD_MASK'] = is_cloudy
+ analysis_mask |= is_cloudy
+ arrays[f'{daily_data_label}_MASK'] = analysis_mask
+ log_difference, zscore, outage = utils.logdiff_outage(
+ log_monthly_data=log_monthly_data,log_daily_data=log_daily_data,
+ analysis_mask=analysis_mask,percentage_drop=percentage_drop
+
+ )
+ arrays[f'{daily_data_label}_LOGDIFF'] = log_difference
+ arrays[f'{daily_data_label}_ZSCORE'] = zscore
+ arrays[f'{daily_data_label}_OUTAGE'] = outage
+
+
+ file_name = utils.get_custom_bbox_label(bbox)
+ outage_tif_path = os.path.join(dst_dir, f'{deliverable}_{file_name}.tif')
+ write_outage_tif(src_arrays=arrays, gt=gt, dst_path=outage_tif_path)
+ # --- 5. UNIFIED DISPLAY & EXPORT ---
+ if display:
+ from rapida.ntl import vis
+ vis.display2(data=arrays,
+ title=f'Outage inputs and results for {deliverable} at {bbox} on {nominal_date.date()}')
diff --git a/rapida/ntl/utils.py b/rapida/ntl/utils.py
new file mode 100644
index 0000000..aec6856
--- /dev/null
+++ b/rapida/ntl/utils.py
@@ -0,0 +1,699 @@
+import math
+import numpy as np
+import rasterio
+from rasterio.transform import Affine
+from rasterio.enums import ColorInterp
+from scipy.ndimage import uniform_filter, gaussian_filter, generic_filter
+import logging
+from scipy.ndimage import label
+from scipy.ndimage import convolve
+from rapida.util.http_get_json import http_get_json
+logger = logging.getLogger('rapida')
+
+
+
+
+def get_custom_bbox_label(bbox: tuple[float, float, float, float]) -> str:
+ minlon, minlat, maxlon, maxlat = bbox
+
+ lon = (minlon + maxlon) * .5
+ lat = (minlat + maxlat) * .5
+
+ # 1. Calculate the longest span of the bbox in degrees
+ max_span = max(maxlon - minlon, maxlat - minlat)
+
+ # 2. Map the size to a Nominatim zoom level (Adjusted for real-world sizes)
+ if max_span > 15.0:
+ zoom = 3 # Country level (Massive areas like the whole USA or Europe)
+ elif max_span > 1.0:
+ zoom = 5 # State/Region level (Catches Puerto Rico's 2.6 span perfectly)
+ elif max_span > 0.2:
+ zoom = 8 # County/District level (Large metro areas)
+ else:
+ zoom = 12 # City/Town/Village level (Small localized bboxes)
+
+ # 3. Add the zoom parameter to the URL
+ url = f"https://nominatim.openstreetmap.org/reverse?lat={lat}&lon={lon}&zoom={zoom}&format=json&accept-language=en"
+
+ headers = {'User-Agent': 'UNDP/RAPIDA'}
+
+ try:
+ data = http_get_json(url=url, timeout=30, headers=headers)
+ address = data.get('address', {})
+
+ country_iso2 = address.get('country_code', 'XX').upper()
+ admin1 = address.get('state', address.get('state_district'))
+ admin2 = address.get('county', address.get('district'))
+ admin3 = address.get('city', address.get('town', address.get('village')))
+
+ # 4. Clean up the string formatting to avoid "US_-"
+ names = []
+ for e in [admin1, admin2, admin3]:
+ if e not in ['', None]:
+ names.append(e.replace(' ', '_'))
+
+ # Join the admins with dashes, then attach the country code
+ admin_str = "-".join(names)
+ if admin_str:
+ return f"{country_iso2}_{admin_str}"
+ else:
+ return country_iso2
+
+ except Exception as e:
+ return f"Error_{e}"
+
+
+def disk_filter(image: np.ndarray, radius: int = 1) -> np.ndarray:
+ """
+ Applies a uniform circular (pillbox) blur.
+ A radius of 1 creates a 3x3 cross. A radius of 2 creates a 5x5 circle.
+ """
+ # 1. Generate a circular mask using an orthogonal grid
+ y, x = np.ogrid[-radius:radius + 1, -radius:radius + 1]
+ mask = x ** 2 + y ** 2 <= radius ** 2
+
+ # 2. Create the kernel and normalize it so it sums to 1.0
+ kernel = mask.astype(np.float64)
+ kernel /= kernel.sum()
+
+ # 3. Convolve the image with the circular kernel
+ return convolve(image, kernel, mode='reflect')
+def get_haze_attenuation_mask(
+ log_diff_raw: np.ndarray,
+ radius: int = 7, # # Radius 7 creates a 15x15 diameter circle
+ max_allowed_std: float = 0.35, # Max variance to be considered "uniform weather"
+ dimming_threshold: float = -0.5 # Minimum regional drop to trigger the check
+) -> np.ndarray:
+ """
+ Identifies unmasked clouds and haze by detecting regions
+ that are uniformly dimming without sharp spatial gradients.
+ """
+ mean_of_sq = disk_filter(log_diff_raw ** 2, radius=radius)
+ sq_of_mean = disk_filter(log_diff_raw, radius=radius) ** 2
+
+ # Prevent negative zeros from floating point precision issues
+ local_variance = np.maximum(mean_of_sq - sq_of_mean, 0.0)
+ local_std = np.sqrt(local_variance)
+
+ # 2. Regional mean difference
+ local_mean_diff = disk_filter(log_diff_raw, radius=radius)
+
+ # 3. Haze classification logic
+ # It must be dropping regionally AND the drop must be extremely uniform
+ is_dropping = local_mean_diff < dimming_threshold
+ is_uniform = local_std < max_allowed_std
+
+
+ # Haze is true ONLY if it's dropping, uniform, AND NOT catastrophic
+ haze_mask = is_dropping & is_uniform
+
+ return haze_mask
+
+
+def sobel(image: np.ndarray) -> np.ndarray:
+ """
+ Applies a Sobel filter using numpy arrays and scipy convolution.
+ """
+ kernel_x = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=np.float64)
+ kernel_y = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], dtype=np.float64)
+
+ gx = convolve(image, kernel_x, mode='reflect')
+ gy = convolve(image, kernel_y, mode='reflect')
+
+ # Calculate hypotenuse: sqrt(gx**2 + gy**2)
+ return np.hypot(gx, gy)
+
+
+def noaa_outage(
+ nrt_array: np.ndarray,
+ baseline_array: np.ndarray,
+ baseline_std: np.ndarray,
+ cmask: np.ndarray,
+ lwm: np.ndarray,
+ baseline_is_log: bool = True,
+ relative_noise_floor: float = 0.15
+) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
+ """
+ Detects outages using pixel-specific Z-scores and global atmospheric shift.
+
+ Args:
+ nrt_array: Near Real-Time VIIRS array (linear scale).
+ baseline_array: Level-3 baseline array.
+ baseline_std: Level-3 baseline standard deviation array.
+ cmask: Resampled Cloud Mask (0=Clear, 1=Probably Clear).
+ lwm: Resampled Land Water Mask (0, 1, 5 = Land).
+ baseline_is_log: True if baseline_array is already np.log1p().
+ relative_noise_floor: Minimum standard deviation limit.
+
+ Returns:
+ final_outages (np.ndarray): Boolean mask of confirmed outages.
+ z_map_display (np.ndarray): Continuous array of Z-scores for visualization.
+ grid_health (np.ndarray): Categorical array (0=Healthy, 1=Dimming, 2=Outage).
+ """
+ # 1. Align Data Spaces
+ a3_log = baseline_array if baseline_is_log else np.log1p(baseline_array)
+ nrt_log = np.log1p(nrt_array)
+
+ # 2. Master Validity Mask
+ is_land = np.isin(lwm, [0, 1, 5])
+ is_clear = (cmask <= 1)
+ has_signal = a3_log > np.log1p(0.5)
+
+ master_mask = is_land & is_clear & has_signal & ~np.isnan(a3_log) & ~np.isnan(nrt_log)
+ #master_mask = is_land & ~np.isnan(a3_log) & ~np.isnan(nrt_log)
+
+ if not np.any(master_mask):
+ logger.warning("No valid clear land pixels found in the target area.")
+ return np.zeros_like(nrt_array, dtype=bool), np.full_like(nrt_array, np.nan), np.zeros_like(nrt_array,
+ dtype=np.uint8)
+
+ # 3. Atmospheric Normalization
+ log_diff = nrt_log - a3_log
+ atmos_shift = np.nanmedian(log_diff[master_mask])
+
+ log_pred = a3_log + atmos_shift
+ log_res = nrt_log - log_pred
+
+ # 4. Pixel-Specific Z-Score
+ a3_linear = np.expm1(a3_log)
+ log_std = np.maximum(baseline_std / (a3_linear + 1.0), relative_noise_floor)
+ z_map_log = log_res / log_std
+
+ # 5. Multi-Tiered Classification Logic
+ is_dimming = (log_res < -0.3) | (z_map_log < -2.0)
+ is_outage = (log_res < -0.7) | (z_map_log < -3.0)
+
+ # 6. Apply the Mask to Build the Heatmap
+ grid_health = np.zeros_like(log_res, dtype=np.uint8)
+ grid_health[is_dimming & master_mask] = 1
+ grid_health[is_outage & master_mask] = 2
+
+ # 7. Spatial Cleanup
+ final_outages = spatial_filter((grid_health == 2), min_size=2)
+
+ # 8. Clean visualization map
+ z_map_display = np.where(master_mask, z_map_log, np.nan)
+
+ return final_outages, z_map_display, grid_health
+
+
+def nasa_outage_old(
+ ntl_log: np.ndarray,
+ ntl_baseline_log: np.ndarray,
+ mask: np.ndarray, # Your 'm' mask: True = Cloud/Water (Ignore)
+ dimming_threshold: float = -0.69, # ~ 25 percent
+ outage_threshold: float = -1
+) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
+ """
+ Calculates outages using spatial smoothing and direct log-differencing.
+
+ Args:
+ ntl_log: Near Real-Time VIIRS array (linear scale).
+ ntl_baseline_log: Level-3 baseline array.
+ mask: Boolean array where True means the pixel should be ignored.
+ baseline_is_log: True if baseline_array is already np.log1p().
+ sigma: Gaussian blur radius to handle registration jitter.
+ dimming_threshold: Log-ratio threshold for dimming events. default e**-0.3= 0.74; then 1 - .74 = .26 26% loss of light
+ outage_threshold: Log-ratio threshold for severe outages. default = e**-0.69 = -.5 ; 1-.5 = .5 50% loss of light
+
+ Returns:
+ final_outages (np.ndarray): Boolean mask of confirmed outages.
+ diff_display (np.ndarray): Continuous array of log-differences for visualization.
+ grid_health (np.ndarray): Categorical array (0=Healthy, 1=Dimming, 2=Outage).
+ """
+ # 1. Align Data Spaces
+ a3_log = ntl_baseline_log
+ nrt_log = ntl_log
+
+ # 2. Master Validity Mask
+ # We invert your invalid_mask (~invalid_mask) so True = Good Pixel
+ has_signal = a3_log > np.log1p(0.5)
+ is_valid = ~mask & has_signal & ~np.isnan(a3_log) & ~np.isnan(nrt_log)
+
+ if not np.any(is_valid):
+ logger.warning("No valid pixels found after applying the invalid mask.")
+ return np.zeros_like(ntl_log, dtype=bool), np.full_like(ntl_log, np.nan), np.zeros_like(ntl_log, dtype=np.uint8)
+
+ # 3. Prevent edge-bleed during smoothing
+ fill_val = np.nanmean(a3_log[is_valid])
+ base_filled = np.where(is_valid, a3_log, fill_val)
+ nrt_filled = np.where(is_valid, nrt_log, fill_val)
+
+ # 4. Apply Gaussian Filter (Handles spatial shifts/jitter)
+ # base_smooth = gaussian_filter(base_filled, sigma=sigma, truncate=3.0)
+ # nrt_smooth = gaussian_filter(nrt_filled, sigma=sigma, truncate=3.0)
+
+ # 4. Apply Disk Filter (The "Smooth but not Squary" method)
+ # radius=1 gives a tight circle (5 pixels). radius=2 gives a wider circle (13 pixels).
+ base_smooth = disk_filter(base_filled, radius=1)
+ nrt_smooth = disk_filter(nrt_filled, radius=1)
+
+ # 5. Direct Log Difference
+ log_diff = nrt_smooth - base_smooth
+
+ is_dimming = (log_diff < dimming_threshold)
+ is_outage = (log_diff < outage_threshold)
+
+ # 7. Apply the Mask to Build the Heatmap
+ grid_health = np.zeros_like(log_diff, dtype=np.uint8)
+ grid_health[is_dimming & is_valid] = 1
+ grid_health[is_outage & is_valid] = 2
+
+ # Since Gaussian blur inherently mitigates spatial jitter,
+ # we pull the boolean mask directly from the grid_health array.
+ final_outages = (grid_health == 2)
+
+ # 8. Clean visualization map
+ diff_display = np.where(is_valid, log_diff, np.nan)
+
+ return diff_display, final_outages, grid_health
+
+
+
+
+
+def nasa_outage(
+ ntl_log: np.ndarray,
+ ntl_baseline_log: np.ndarray,
+ baseline_std: np.ndarray, # <-- NEW: A3 _Std band required here
+ baseline_num: np.ndarray, # <-- NEW: A3 _Num band required here
+ no_cloud:np.ndarray,
+ mask: np.ndarray,
+ dimm_perc_drop = 50,
+ outage_perc_drop = 80
+) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
+ """
+ Calculates outages using spatial smoothing and variance-scaled thresholding.
+ """
+ a3_log = ntl_baseline_log
+ nrt_log = ntl_log
+
+ # 1. Gate
+ #is_valid = ~mask & ~np.isnan(a3_log) & ~np.isnan(nrt_log)# & (a3_log>0)
+ is_valid = ~np.isnan(nrt_log) & ~np.isnan(a3_log) & ~mask
+ if not np.any(is_valid):
+ logger.warning("No valid pixels found.")
+ return np.full_like(ntl_log, np.nan), np.zeros_like(ntl_log, dtype=bool), np.zeros_like(ntl_log,
+ dtype=np.uint8), np.zeros_like(
+ ntl_log, dtype=np.float32)
+
+ # 2. Prevent edge-bleed during smoothing
+ fill_val = np.nanmean(a3_log[is_valid])
+ base_filled = np.where(is_valid, a3_log, fill_val)
+ nrt_filled = np.where(is_valid, nrt_log, fill_val)
+
+ # 3. Spatial Smoothing
+ base_smooth = disk_filter(base_filled, radius=1)
+ nrt_smooth = disk_filter(nrt_filled, radius=1)
+
+ # 4. Direct Log Difference
+ log_diff = np.zeros_like(ntl_log, dtype=np.float32)
+ log_diff[is_valid] = nrt_smooth[is_valid] - base_smooth[is_valid]
+
+
+ log_outage_threshold = np.log(1.0 - (outage_perc_drop / 100.0))
+ log_dimm_threshold = np.log(1.0 - (dimm_perc_drop / 100.0))
+
+ # 6. Apply Dynamic Classification
+ # We are now comparing the log_diff array against the dynamic threshold arrays
+ grid_health = np.zeros_like(ntl_log, dtype=np.uint8)
+ grid_health[is_valid & (log_diff < log_dimm_threshold)] = 1
+ grid_health[is_valid & (log_diff < log_outage_threshold)] = 2
+
+ final_outages = (grid_health == 2)
+
+ # --- PHYSICAL CONFIDENCE SCORE ---
+
+ # 7. Compute Confidence adapted for dynamic thresholds
+ confidence = np.zeros_like(log_diff, dtype=np.float32)
+
+ outage_pixels = (grid_health == 2) & is_valid
+
+ # 1. Evidence: Signal-to-Noise Ratio (Is the drop larger than local noise?)
+ # We use a 3-sigma rule (SNR=3.0 is 100% certainty)
+ std_safe = np.where(baseline_std < 0.05, 0.05, baseline_std) # Floor to avoid div by zero
+ snr = np.abs(log_diff[outage_pixels]) / std_safe[outage_pixels]
+ conf_snr = np.clip(snr / 3.0, 0.0, 1.0)
+
+ # 2. Evidence: Baseline Robustness (Do we have enough history?)
+ # Assume 'a3_num' is the count of clear days from your VNP46A3 product.
+ # 15 days is high confidence, 1 day is low.
+ conf_history = np.clip(baseline_num[outage_pixels] / 15.0, 0.0, 1.0)
+
+ # 3. Evidence: Observability (Was the NTL pixel actually observed clearly?)
+ # 1.0 if Clear, 0.0 if Cloud-Contaminated/Shadow
+ conf_obs = np.where(no_cloud[outage_pixels] == 0, 1.0, 0.0)
+
+ # FINAL CONFIDENCE: The product of all evidences
+ # If any evidence is weak, the total confidence drops.
+ confidence[outage_pixels] = conf_snr * conf_history * conf_obs
+
+ # 8. Clean visualization map
+ diff_display = np.where(is_valid, log_diff, np.nan)
+
+ return diff_display, final_outages, grid_health, confidence
+
+def logdiff_outage_orig(
+ baseline_log_array: np.ndarray,
+ nrt_log_array: np.ndarray,
+ cloud_mask: np.ndarray, # Boolean array: True where clouds exist
+ sigma: float = 1.25,
+ percentage_drop: float = 50
+) -> np.ndarray:
+ """
+ Calculates outages using direct log-differencing, which mathematically
+ represents proportional radiance loss.
+ """
+ remaining_fraction = 1.0 - (percentage_drop / 100.0)
+ log_drop_threshold = np.log(remaining_fraction)
+
+ # 1. Strict Exclusion (No Data is better than Bad Data)
+ is_valid = ~np.isnan(nrt_log_array) & ~np.isnan(baseline_log_array) & ~cloud_mask
+
+ # 2. Prevent edge-bleed during smoothing (using your fill method)
+ fill_val = np.nanmean(baseline_log_array[is_valid])
+ base_filled = np.where(is_valid, baseline_log_array, fill_val)
+ nrt_filled = np.where(is_valid, nrt_log_array, fill_val)
+
+ # 3. Apply Gaussian Filter (Handles spatial shifts/jitter)
+ # base_smooth = gaussian_filter(base_filled, sigma=sigma, truncate=3.0)
+ # nrt_smooth = gaussian_filter(nrt_filled, sigma=sigma, truncate=3.0)
+
+ # 3. Spatial Smoothing
+ base_smooth = disk_filter(base_filled, radius=1)
+ nrt_smooth = disk_filter(nrt_filled, radius=1)
+
+ # 4. Direct Log Difference (This IS the ratio!)
+ # E.g., if NRT is half the Baseline, log1p(NRT) - log1p(Base) is roughly -0.69
+ log_diff = nrt_smooth - base_smooth
+ # 5. Mask out invalid areas from the final result
+ log_diff = np.where(is_valid, log_diff, np.nan)
+
+ # 6. Define the Outage Boolean Map
+ # We only care about areas that dropped by more than our threshold
+ outage_map = log_diff < log_drop_threshold
+
+ # Optional: You can return log_diff for continuous analysis (severity),
+ # or outage_map for binary polygons.
+ return log_diff, outage_map
+
+
+
+
+def logdiff_outage(
+ log_monthly_data: np.ndarray,
+ log_daily_data: np.ndarray,
+ analysis_mask: np.ndarray, # Boolean array: True where mask wil be applied
+ percentage_drop: float = 50.0,
+ z_threshold: float = -2,
+ relative_noise_floor: float = 0.15
+) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
+ """
+ Calculates outages using a hybrid approach of direct log-differencing and
+ localized spatial Z-scores to handle VNP46A1 noise and viewing geometry.
+
+ Args:
+ log_monthly_data: Log-scaled baseline (np.log1p of radiance).
+ log_daily_data: Log-scaled Near Real-Time daily observation.
+ analysis_mask: Boolean mask where True represents invalid/cloudy pixels.
+ percentage_drop: Physical radiance drop threshold (e.g., 50 means a 50% drop).
+ z_threshold: Spatial significance threshold (number of local standard deviations).
+ relative_noise_floor: Minimum standard deviation limit to avoid zero-division in dark regions.
+
+ Returns:
+ log_diff (np.ndarray): Continuous array of log differences for continuous severity analysis.
+ z_score (np.ndarray): Localized spatial Z-scores for visualization.
+ outage_map (np.ndarray): Clean boolean mask of confirmed crisis outages.
+ """
+ # Proportional loss mapping
+ remaining_fraction = 1.0 - (percentage_drop / 100.0)
+ log_drop_threshold = np.log(remaining_fraction)
+
+ # 1. Master Validity Mask
+ is_valid = ~np.isnan(log_daily_data) & ~np.isnan(log_monthly_data) & ~analysis_mask
+
+ if not np.any(is_valid):
+ return (
+ np.full_like(log_daily_data, np.nan),
+ np.full_like(log_daily_data, np.nan),
+ np.zeros_like(log_daily_data, dtype=bool)
+ )
+
+ # 2. Prevent edge-bleed during spatial smoothing
+ fill_val = np.nanmean(log_monthly_data[is_valid])
+ base_filled = np.where(is_valid, log_monthly_data, fill_val)
+ nrt_filled = np.where(is_valid, log_daily_data, fill_val)
+
+ # 3. Spatial Smoothing (Using 1-pixel radius disk equivalent / uniform 3x3)
+ # Using generic_filter or uniform_filter here keeps dependencies clean
+ base_smooth = disk_filter(base_filled, radius=2)
+ nrt_smooth = disk_filter(nrt_filled, radius=2)
+
+ # 4. Direct Log Difference (Physical Ratio Estimation)
+ log_diff_raw = nrt_smooth - base_smooth
+
+
+ # 5. Dynamic Local Spatial Noise Floor Estimation
+ # Calculates standard deviation inside a 3x3 window on the smoothed baseline
+ local_baseline_std = generic_filter(base_smooth, np.std, size=3)
+
+ # Convert linear relative noise floor scale to approximate log space scaling
+ # log_std approx std / (linear_val + 1)
+ baseline_linear = np.expm1(base_smooth)
+ log_std = np.maximum(local_baseline_std / (baseline_linear + 1.0), relative_noise_floor)
+ #log_std = np.maximum(local_baseline_std, relative_noise_floor)
+
+ # 6. Calculate Spatial Z-Score
+ z_score_raw = log_diff_raw / log_std
+
+ # 7. Mask out invalid/cloudy/water pixels across arrays
+ log_diff = np.where(is_valid, log_diff_raw, np.nan)
+ z_score = np.where(is_valid, z_score_raw, np.nan)
+
+ # 8. Dual-Engine Outage Mapping
+ # Triggers only if it passes BOTH the absolute physical drop AND the local spatial statistical variance
+ outage_map = (log_diff < log_drop_threshold) & (z_score < z_threshold)
+
+ return log_diff, z_score, outage_map
+
+def rigorous_ssim(img1, img2, data_range=12.0) -> np.ndarray:
+ """
+ Computes standard Wang et al. (2004) SSIM with safety checks for
+ MaskedArrays and floating-point catastrophic cancellation.
+ """
+ # 1. Safely extract masked data and force 64-bit precision
+ img1 = img1.filled(0.0) if hasattr(img1, 'filled') else img1
+ img2 = img2.filled(0.0) if hasattr(img2, 'filled') else img2
+
+ img1 = img1.astype(np.float64)
+ img2 = img2.astype(np.float64)
+
+ # 2. Lock the stability constants globally
+ K1, K2 = 0.01, 0.03
+ C1 = (K1 * data_range) ** 2
+ C2 = (K2 * data_range) ** 2
+
+ sigma = 1.5
+ trunc = 3.5
+
+ # 3. Compute local means
+ mu1 = gaussian_filter(img1, sigma=sigma, truncate=trunc)
+ mu2 = gaussian_filter(img2, sigma=sigma, truncate=trunc)
+
+ mu1_sq = mu1 ** 2
+ mu2_sq = mu2 ** 2
+ mu1_mu2 = mu1 * mu2
+
+ # 4. Compute local variances & CLAMP to 0 to prevent float precision collapse
+ sigma1_sq = gaussian_filter(img1 ** 2, sigma=sigma, truncate=trunc) - mu1_sq
+ sigma2_sq = gaussian_filter(img2 ** 2, sigma=sigma, truncate=trunc) - mu2_sq
+ sigma12 = gaussian_filter(img1 * img2, sigma=sigma, truncate=trunc) - mu1_mu2
+
+ sigma1_sq = np.maximum(0, sigma1_sq)
+ sigma2_sq = np.maximum(0, sigma2_sq)
+
+ # 5. Compute full SSIM map
+ num = (2 * mu1_mu2 + C1) * (2 * sigma12 + C2)
+ den = (mu1_sq + mu2_sq + C1) * (sigma1_sq + sigma2_sq + C2)
+ ssim_map = num / den
+
+ return ssim_map
+
+def pure_numpy_ssim(nrt_masked: np.ma.MaskedArray, baseline_masked: np.ma.MaskedArray, win_size: int = 7) -> np.ndarray:
+ """
+ Computes the Structural Similarity Index (SSIM) between two images
+ using purely NumPy and SciPy uniform filters.
+
+ Returns a 2D NumPy array matching the input dimensions (values from -1 to 1).
+ """
+ # 1. Fill masked regions with 0.0 so they don't break the rolling windows
+ img1 = nrt_masked.filled(0.0)
+ img2 = baseline_masked.filled(0.0)
+
+ # 2. Dynamic range stability constants (C1, C2)
+ # Based on log1p values, data max is roughly 9.0 to 11.0
+ data_range = max(img1.max(), img2.max()) - min(img1.min(), img2.min())
+ K1, K2 = 0.01, 0.03
+ C1 = (K1 * data_range) ** 2
+ C2 = (K2 * data_range) ** 2
+
+ # 3. Compute local means (mu) using a uniform box filter
+ mu1 = uniform_filter(img1, size=win_size, mode='reflect')
+ mu2 = uniform_filter(img2, size=win_size, mode='reflect')
+
+ mu1_sq = mu1 ** 2
+ mu2_sq = mu2 ** 2
+ mu1_mu2 = mu1 * mu2
+
+ # 4. Compute local variances (sigma_sq) and covariance (sigma_12)
+ # Formula: Var(X) = E[X^2] - (E[X])^2
+ sigma1_sq = uniform_filter(img1 ** 2, size=win_size) - mu1_sq
+ sigma2_sq = uniform_filter(img2 ** 2, size=win_size) - mu2_sq
+ sigma12 = uniform_filter(img1 * img2, size=win_size) - mu1_mu2
+
+ # 5. Compute full SSIM map
+ num = (2 * mu1_mu2 + C1) * (2 * sigma12 + C2)
+ den = (mu1_sq + mu2_sq + C1) * (sigma1_sq + sigma2_sq + C2)
+ ssim_map = num / den
+
+ return ssim_map
+
+def get_intersecting_tiles(bbox: tuple[float, float, float, float]) -> list[tuple[int, int]]:
+ """
+ Identifies VIIRS Sinusoidal tiles (h, v) intersecting a geographic bounding box.
+ bbox format: (min_lon, min_lat, max_lon, max_lat)
+ :return tuple of ints representing pairs of tile coordinates (horizontal, vertical)
+ """
+ min_lon, min_lat, max_lon, max_lat = bbox
+
+ # VIIRS standard sinusoidal grid is approx 10x10 degrees at the equator
+ # h runs 0 to 35 (180W to 180E)
+ # v runs 0 to 17 (90N to 90S)
+ h_min = math.floor((min_lon + 180) / 10)
+ h_max = math.floor((max_lon + 180) / 10)
+ v_min = math.floor((90 - max_lat) / 10)
+ v_max = math.floor((90 - min_lat) / 10)
+
+ tiles = []
+ for v in range(max(0, v_min), min(18, v_max + 1)):
+ for h in range(max(0, h_min), min(36, h_max + 1)):
+ tiles.append(f'h{h:02d}v{v:02d}')
+
+ return tiles
+
+TIMESTAMP_FORMATS = {
+ "A1": "%Y%m%d", # Daily: Year + Julian Day (e.g., 2026134)
+ "A2": "%Y%m%d", # Daily: Year + Julian Day (e.g., 2026134)
+ "A3": "%Y%m", # Monthly: Year + Month (e.g., 202605)
+ "A4": "%Y" # Yearly: Year only (e.g., 2026)
+}
+
+def timestamp_format(product_id: str) -> str:
+ """Determine the correct temporal format string based on product name."""
+ # Check if A1, A2, A3, or A4 is in the product string (e.g., 'VNP46A1')
+ for identifier, time_format in TIMESTAMP_FORMATS.items():
+ if identifier in product_id:
+ return time_format
+
+
+def write_outage_tif(src_arrays:dict[str, np.array]=None, gt:list = None, dst_path:str=None ) -> bool:
+ transform = Affine.from_gdal(*gt)
+ label, ar = next(iter(src_arrays.items()))
+ height, width = ar.shape
+ with rasterio.open(dst_path, mode='w',driver='GTiff',height=height,width=width,
+ count=len(src_arrays),
+ dtype='float32',
+ crs='EPSG:4326',
+ transform=transform) as dst:
+ dst.update_tags(INTERLEAVE='PIXEL')
+ dst.colorinterp = [ColorInterp.undefined] * len(src_arrays)
+
+ for i, e in enumerate(src_arrays.items(), start=1):
+ label, array = e
+ dst.write(array.astype('float32'), i)
+ dst.set_band_description(i, label)
+
+
+
+def spatial_filter(outage_map, min_size=2):
+ # 1. Group connected pixels into "clumps"
+ labeled_array, num_features = label(outage_map)
+
+ # 2. Count how many pixels are in each clump
+ clump_sizes = np.bincount(labeled_array.ravel())
+
+ # 3. Create a mask of clumps that meet your size requirement
+ mask_size = clump_sizes >= min_size
+
+ # 4. Filter the original map (clump 0 is the background, so we ignore it)
+ mask_size[0] = 0
+ return mask_size[labeled_array]
+
+
+
+
+def wavelet_continuous_persistence(
+ monthly: np.ndarray,
+ daily: np.ndarray,
+ lw_bg: np.ndarray
+) -> tuple[np.ndarray, np.ndarray]:
+ import pywt
+ # 1. Build the Standard NumPy Exclusion Mask
+ master_mask = ~np.isin(lw_bg, [0, 1, 5]) | np.isnan(monthly) | np.isnan(daily)
+
+ # 2. Condition and stabilize inputs
+ noise_floor = 0.2
+ monthly_clean = np.where(~master_mask, np.maximum(monthly, 0.0), 0.0)
+ daily_clean = np.where(~master_mask, np.maximum(daily, 0.0), 0.0)
+
+ max_level = pywt.swt_max_level(min(daily.shape))
+ target_level = min(5, max_level)
+
+ # This will hold the continuous sum of all loss fractions across scales
+ cumulative_loss_map = np.zeros_like(daily, dtype=np.float32)
+
+ # 3. Execute the Multi-Scale Integration
+ for l in range(1, target_level + 1):
+ try:
+ swt_base = pywt.swt2(monthly_clean, 'db4', level=l)
+ swt_nrt = pywt.swt2(daily_clean, 'db4', level=l)
+
+ cA_base_full = swt_base[0][0]
+ cA_nrt_full = swt_nrt[0][0]
+ except ValueError:
+ break
+
+ lit_mask = cA_base_full > 1.0
+
+ # Calculate continuous physical fraction (0.0 to 1.0+)
+ loss_fraction = np.divide(
+ (cA_base_full - cA_nrt_full),
+ (cA_base_full + noise_floor),
+ out=np.zeros_like(cA_base_full),
+ where=lit_mask
+ )
+
+ # Compound the raw physical loss directly!
+ cumulative_loss_map += np.where(lit_mask & ~master_mask, loss_fraction, 0)
+
+ # 4. Final Cleanup
+ # We now have a continuous spectrum where high values definitively equal deep structural collapses
+ continuous_spectrum = np.where(~master_mask, cumulative_loss_map, 0.0)
+ # =========================================================================
+ # THE SELF-ADAPTING GATEKEEPER (Dynamic Statistical Threshold)
+ # =========================================================================
+ # Isolate valid pixels that experienced at least *some* baseline drop
+ valid_signals = continuous_spectrum[~master_mask & (continuous_spectrum > 0.01)]
+
+ if len(valid_signals) > 0:
+ # Calculate the ambient structural noise floor of THIS specific image
+ scene_mean = np.mean(valid_signals)
+ scene_std = np.std(valid_signals)
+
+ # A blackout is defined mathematically as a 3-sigma anomaly (99.7% confidence)
+ dynamic_threshold = scene_mean + (1.0 * scene_std)
+ else:
+ dynamic_threshold = 0.0
+ print(dynamic_threshold)
+ outage_map = (continuous_spectrum > dynamic_threshold) & ~master_mask
+ return continuous_spectrum, outage_map, master_mask
\ No newline at end of file
diff --git a/rapida/ntl/vis.py b/rapida/ntl/vis.py
new file mode 100644
index 0000000..e40e70b
--- /dev/null
+++ b/rapida/ntl/vis.py
@@ -0,0 +1,193 @@
+from matplotlib import pyplot as plt
+from matplotlib import gridspec
+from matplotlib.colors import ListedColormap, BoundaryNorm
+from mpl_toolkits.axes_grid1 import make_axes_locatable
+import numpy as np
+
+def display(data=dict(), interpolation='nearest', title=''):
+ """
+ Displays one or more arrays with legend.
+ @args
+ @da - dictionary or ordered dictionary if one wants to preserve the order of arrays, ex {'name:'np.array(2D)}
+
+ """
+ fig = plt.figure()
+ ncols = 2 # we want two columns
+ nrows = int((len(data) / 2) + 1)
+
+ gs = gridspec.GridSpec(nrows=nrows, ncols=ncols )
+
+ for i, v in enumerate(data.items()):
+ iname, a = v
+ if i == 0:
+
+ ax = fig.add_subplot(gs[i])
+ fax = ax
+ ax.set_title(iname)
+ else:
+ ax = fig.add_subplot(gs[i], sharex=fax, sharey=fax)
+ ax.set_title(iname)
+
+ im = ax.imshow(a, interpolation=interpolation)
+ plt.colorbar(im, use_gridspec=True, orientation='vertical')
+ ax.set_aspect('equal')
+ # fig.show()
+ plt.tight_layout()
+ fig.suptitle(title)
+ plt.show()
+
+
+
+def display1(data=dict(), interpolation='nearest', title=''):
+ """
+ Improved display function that maximizes screen real estate and
+ ensures perfectly aligned subplots.
+ """
+ n = len(data)
+ if n == 0: return
+
+ # 1. Calculate a better figure size based on the number of plots
+ # (Width, Height) - 16x10 is standard for modern monitors
+ ncols = 2
+ nrows = int(np.ceil(n / ncols))
+
+ # We force a large figure size so it fills the screen
+ fig, axes = plt.subplots(nrows=nrows, ncols=ncols,
+ figsize=(16, 5 * nrows),
+ constrained_layout=True,
+ squeeze=False)
+
+ # Flatten axes for easy iteration
+ axes_flat = axes.flatten()
+ fax = None # For sharing axis
+
+ for i, (iname, a) in enumerate(data.items()):
+ ax = axes_flat[i]
+
+ # Share axis logic to keep zoom synced
+ if i == 0:
+ fax = ax
+ else:
+ # We recreate the subplot behavior to share axes dynamically
+ ax.sharex(fax)
+ ax.sharey(fax)
+
+ # 2. Display the image
+ # 'magma' is usually the best for NTL (Nighttime Lights)
+ cmap = 'viridis' if 'Mask' in iname else 'magma'
+ im = ax.imshow(a, interpolation=interpolation, cmap=cmap)
+
+ ax.set_title(iname, fontsize=14, fontweight='bold')
+
+ # 3. FIX: Colorbar placement that doesn't 'squish' the plot
+ divider = make_axes_locatable(ax)
+ cax = divider.append_axes("right", size="5%", pad=0.1)
+ plt.colorbar(im, cax=cax)
+
+ # Keep the geographic shape correct
+ ax.set_aspect('equal')
+
+ # Hide any unused subplots (if n is odd)
+ for j in range(i + 1, len(axes_flat)):
+ axes_flat[j].axis('off')
+
+ fig.suptitle(title, fontsize=20)
+ plt.show()
+
+
+def plot(array):
+ # 1. Convert to NanoWatts and clean (The magic fix for satellite data)
+ import matplotlib.pyplot as plt
+
+
+
+ # 2. The Matplotlib Plot
+ plt.figure(figsize=(10, 8))
+
+ # imshow is perfect for 2D spatial arrays
+ # 'magma' or 'inferno' are great colormaps for night lights
+ img = plt.imshow(array, cmap='magma', interpolation='nearest')
+
+ plt.colorbar(img, label='')
+ plt.title("Night Lights - Zero Drama Edition")
+
+ plt.show()
+
+
+def display2(data=dict(), interpolation='nearest', title='', max_discrete_vals=5):
+ """
+ Improved display function that maximizes screen real estate,
+ ensures perfectly aligned subplots, and automatically detects
+ discrete classification maps to build custom legends.
+ """
+ n = len(data)
+ if n == 0: return
+
+ # 1. Calculate a better figure size
+ ncols = 2
+ nrows = int(np.ceil(n / ncols))
+
+ fig, axes = plt.subplots(nrows=nrows, ncols=ncols,
+ figsize=(16, 5 * nrows),
+ constrained_layout=True,
+ squeeze=False)
+
+ axes_flat = axes.flatten()
+ fax = None
+
+ for i, (iname, a) in enumerate(data.items()):
+ ax = axes_flat[i]
+
+ # Share axis logic to keep zoom synced
+ if i == 0:
+ fax = ax
+ else:
+ ax.sharex(fax)
+ ax.sharey(fax)
+
+ ax.set_title(iname, fontsize=14, fontweight='bold')
+ ax.set_aspect('equal')
+
+ # --- THE NEW DYNAMIC LOGIC ---
+ # Filter out NaNs to safely check unique values
+ valid_vals = a[~np.isnan(a)]
+ unique_vals = np.unique(valid_vals)
+
+ # Check if this is a discrete/classification map (e.g., grid_health or masks)
+ if len(unique_vals) <= max_discrete_vals:
+ # It's discrete! Use specific colors and a custom legend.
+ # (Matches your Black/Yellow/Red layout for 3 values)
+ discrete_palette = ['black', 'yellow', 'red', 'cyan', 'magenta']
+ colors = discrete_palette[:len(unique_vals)]
+ cmap = ListedColormap(colors)
+
+ # BoundaryNorm ensures the colorbar is split perfectly by the number of classes
+ bounds = np.arange(len(unique_vals) + 1) - 0.5
+ norm = BoundaryNorm(bounds, cmap.N)
+
+ # Force interpolation='none' so classes don't blur at the edges
+ im = ax.imshow(a, interpolation='none', cmap=cmap, norm=norm)
+
+ divider = make_axes_locatable(ax)
+ cax = divider.append_axes("right", size="5%", pad=0.1)
+
+ # Build a discrete colorbar with centered ticks
+ cbar = plt.colorbar(im, cax=cax, ticks=np.arange(len(unique_vals)))
+ # Label the ticks with their actual array values
+ cbar.ax.set_yticklabels([f'Val: {v}' for v in unique_vals])
+
+ else:
+ # It's continuous! (e.g., raw radiance or log_diff)
+ cmap = 'viridis' if 'Mask' in iname else 'magma'
+ im = ax.imshow(a, interpolation=interpolation, cmap=cmap)
+
+ divider = make_axes_locatable(ax)
+ cax = divider.append_axes("right", size="5%", pad=0.1)
+ plt.colorbar(im, cax=cax)
+
+ # Hide any unused subplots
+ for j in range(i + 1, len(axes_flat)):
+ axes_flat[j].axis('off')
+
+ fig.suptitle(title, fontsize=20)
+ plt.show()
\ No newline at end of file
diff --git a/rapida/util/gpd_overlay.py b/rapida/util/gpd_overlay.py
index 0b11064..b1e20b3 100644
--- a/rapida/util/gpd_overlay.py
+++ b/rapida/util/gpd_overlay.py
@@ -55,7 +55,7 @@ def process_chunk(chunk_rows, overlay_df, overlay_df_sindex, crs):
single_row_gdf = gpd.GeoDataFrame([row], geometry='geometry', crs=crs)
poly_geom = single_row_gdf.geometry.iloc[0]
- possible_matches_index = list(overlay_df_sindex.intersection(poly_geom.bounds))
+ possible_matches_index = list(overlay_df_sindex.intersection(poly_geom.bounds_from_file))
possible_matches = overlay_df.iloc[possible_matches_index]
precise_matches = possible_matches[possible_matches.intersects(poly_geom)].copy()
precise_matches.loc[:, "geometry"] = precise_matches.geometry.intersection(poly_geom)
diff --git a/rapida/util/http_get_json.py b/rapida/util/http_get_json.py
index 9099a7e..33cff2a 100644
--- a/rapida/util/http_get_json.py
+++ b/rapida/util/http_get_json.py
@@ -4,7 +4,7 @@
logger = logging.getLogger(__name__)
-def http_get_json(url=None, timeout=None):
+def http_get_json(url=None, timeout=None, **kwargs):
"""
Generic HTTP get function using httpx
:param url: str, the url to be fetched
@@ -12,7 +12,7 @@ def http_get_json(url=None, timeout=None):
:return: python dict representing the result as parsed json
"""
assert timeout is not None, f'Invalid timeout={timeout}'
- with httpx.Client(timeout=timeout) as client:
+ with httpx.Client(timeout=timeout, **kwargs) as client:
response = client.get(url)
response.raise_for_status()
if response.status_code == 200:
diff --git a/uv.lock b/uv.lock
index 58fa1c8..17a7369 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2,13 +2,16 @@ version = 1
revision = 3
requires-python = ">=3.10"
resolution-markers = [
- "python_full_version >= '3.14' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
"python_full_version >= '3.14' and sys_platform == 'darwin'",
"python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
- "python_full_version == '3.13.*' and sys_platform == 'win32'",
- "python_full_version == '3.12.*' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version == '3.13.*' and sys_platform == 'emscripten'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten'",
"python_full_version == '3.13.*' and sys_platform == 'darwin'",
@@ -17,14 +20,16 @@ resolution-markers = [
"python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
"(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
- "python_full_version == '3.11.*' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version == '3.11.*' and sys_platform == 'emscripten'",
"python_full_version == '3.11.*' and sys_platform == 'darwin'",
"python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
"python_full_version < '3.11' and sys_platform == 'darwin'",
"python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')",
+ "python_full_version < '3.11' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_machine != 'ARM64' and sys_platform == 'win32') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')",
]
[[package]]
@@ -239,8 +244,8 @@ name = "astunparse"
version = "1.6.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "six" },
- { name = "wheel" },
+ { name = "six", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "wheel", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f3/af/4182184d3c338792894f34a62672919db7ca008c89abee9b564dd34d8029/astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872", size = 18290, upload-time = "2019-12-22T18:12:13.129Z" }
wheels = [
@@ -263,7 +268,8 @@ source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.11' and sys_platform == 'darwin'",
"python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')",
+ "python_full_version < '3.11' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_machine != 'ARM64' and sys_platform == 'win32') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')",
]
dependencies = [
{ name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" },
@@ -278,13 +284,16 @@ name = "asyncclick"
version = "8.3.0.7"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
- "python_full_version >= '3.14' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
"python_full_version >= '3.14' and sys_platform == 'darwin'",
"python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
- "python_full_version == '3.13.*' and sys_platform == 'win32'",
- "python_full_version == '3.12.*' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version == '3.13.*' and sys_platform == 'emscripten'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten'",
"python_full_version == '3.13.*' and sys_platform == 'darwin'",
@@ -293,7 +302,8 @@ resolution-markers = [
"python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
"(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
- "python_full_version == '3.11.*' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version == '3.11.*' and sys_platform == 'emscripten'",
"python_full_version == '3.11.*' and sys_platform == 'darwin'",
"python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
@@ -466,6 +476,57 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
]
+[[package]]
+name = "cftime"
+version = "1.6.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/65/dc/470ffebac2eb8c54151eb893055024fe81b1606e7c6ff8449a588e9cd17f/cftime-1.6.5.tar.gz", hash = "sha256:8225fed6b9b43fb87683ebab52130450fc1730011150d3092096a90e54d1e81e", size = 326605, upload-time = "2025-10-13T18:56:26.352Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/45/dcc38d7b293107d3e33b3d94b2619687eb414a4f16880e2e841cdb6ac49a/cftime-1.6.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ad81e8cb0eb873b33c3d1e22c6168163fdc64daa8f7aeb4da8092f272575f4d", size = 510221, upload-time = "2025-10-13T18:55:52.976Z" },
+ { url = "https://files.pythonhosted.org/packages/68/63/2875341516fcfe80f1a16f86b420aec9441223ab5381d554441c9fdae56e/cftime-1.6.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12d95c6af852114a13301c5a61e41afdbd1542e72939c1083796f8418b9b8b0e", size = 490684, upload-time = "2025-10-13T18:55:54.685Z" },
+ { url = "https://files.pythonhosted.org/packages/80/7f/85f2c4c7ae8300b7871af7d7d144ad06f71dc0dd6258f0d18fd966067d1b/cftime-1.6.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2659b7df700e27d9e3671f686ce474dfb5fc274966961edf996acc148dfa094a", size = 1592268, upload-time = "2025-10-13T19:39:10.992Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/9a/72dbd72498e958edf41a770bbd05e68141774325a945092059f4eb9c653d/cftime-1.6.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:94cebdfcda6a985b8e69aed22d00d6b8aa1f421495adbdcff1d59b3e896d81e2", size = 1624716, upload-time = "2025-10-13T18:55:55.848Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/9e/2c4c720ad8bbe87994ca62a0e3c09d3786b984af664a91a6f3a668aa0b13/cftime-1.6.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:179681b023349a2fe277ceccc89d4fc52c0dd105cb59b7187b5bc5d442875133", size = 1705927, upload-time = "2025-10-13T18:55:57.711Z" },
+ { url = "https://files.pythonhosted.org/packages/da/77/66484061dee5fbcb2fdcfa6a491d4efb880725117f4a339d20a5323105df/cftime-1.6.5-cp310-cp310-win_amd64.whl", hash = "sha256:d8b9fdecb466879cfe8ca4472b229b6f8d0bb65e4ffd44266ae17484bac2cf38", size = 472435, upload-time = "2025-10-13T18:55:59.092Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/f6/9da7aba9548ede62d25936b8b448acd7e53e5dcc710896f66863dcc9a318/cftime-1.6.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:474e728f5a387299418f8d7cb9c52248dcd5d977b2a01de7ec06bba572e26b02", size = 512733, upload-time = "2025-10-13T18:56:00.189Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/d5/d86ad95fc1fd89947c34b495ff6487b6d361cf77500217423b4ebcb1f0c2/cftime-1.6.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ab9e80d4de815cac2e2d88a2335231254980e545d0196eb34ee8f7ed612645f1", size = 492946, upload-time = "2025-10-13T18:56:01.262Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/93/d7e8dd76b03a9d5be41a3b3185feffc7ea5359228bdffe7aa43ac772a75b/cftime-1.6.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ad24a563784e4795cb3d04bd985895b5db49ace2cbb71fcf1321fd80141f9a52", size = 1689856, upload-time = "2025-10-13T19:39:12.873Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/8d/86586c0d75110f774e46e2bd6d134e2d1cca1dedc9bb08c388fa3df76acd/cftime-1.6.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a3cda6fd12c7fb25eff40a6a857a2bf4d03e8cc71f80485d8ddc65ccbd80f16a", size = 1718573, upload-time = "2025-10-13T18:56:02.788Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/fe/7956914cfc135992e89098ebbc67d683c51ace5366ba4b114fef1de89b21/cftime-1.6.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:28cda78d685397ba23d06273b9c916c3938d8d9e6872a537e76b8408a321369b", size = 1788563, upload-time = "2025-10-13T18:56:04.075Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/c7/6669708fcfe1bb7b2a7ce693b8cc67165eac00d3ac5a5e8f6ce1be551ff9/cftime-1.6.5-cp311-cp311-win_amd64.whl", hash = "sha256:93ead088e3a216bdeb9368733a0ef89a7451dfc1d2de310c1c0366a56ad60dc8", size = 473631, upload-time = "2025-10-13T18:56:05.159Z" },
+ { url = "https://files.pythonhosted.org/packages/82/c5/d70cb1ab533ca790d7c9b69f98215fa4fead17f05547e928c8f2b8f96e54/cftime-1.6.5-cp311-cp311-win_arm64.whl", hash = "sha256:3384d69a0a7f3d45bded21a8cbcce66c8ba06c13498eac26c2de41b1b9b6e890", size = 459383, upload-time = "2026-01-02T21:16:47.317Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/c1/e8cb7f78a3f87295450e7300ebaecf83076d96a99a76190593d4e1d2be40/cftime-1.6.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:eef25caed5ebd003a38719bd3ff8847cd52ef2ea56c3ebdb2c9345ba131fc7c5", size = 504175, upload-time = "2025-10-13T18:56:06.398Z" },
+ { url = "https://files.pythonhosted.org/packages/50/1a/86e1072b09b2f9049bb7378869f64b6747f96a4f3008142afed8955b52a4/cftime-1.6.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c87d2f3b949e45463e559233c69e6a9cf691b2b378c1f7556166adfabbd1c6b0", size = 485980, upload-time = "2025-10-13T18:56:08.669Z" },
+ { url = "https://files.pythonhosted.org/packages/35/28/d3177b60da3f308b60dee2aef2eb69997acfab1e863f0bf0d2a418396ce5/cftime-1.6.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:82cb413973cc51b55642b3a1ca5b28db5b93a294edbef7dc049c074b478b4647", size = 1591166, upload-time = "2025-10-13T19:39:14.109Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/fd/a7266970312df65e68b5641b86e0540a739182f5e9c62eec6dbd29f18055/cftime-1.6.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85ba8e7356d239cfe56ef7707ac30feaf67964642ac760a82e507ee3c5db4ac4", size = 1642614, upload-time = "2025-10-13T18:56:09.815Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/73/f0035a4bc2df8885bb7bd5fe63659686ea1ec7d0cc74b4e3d50e447402e5/cftime-1.6.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:456039af7907a3146689bb80bfd8edabd074c7f3b4eca61f91b9c2670addd7ad", size = 1688090, upload-time = "2025-10-13T18:56:11.442Z" },
+ { url = "https://files.pythonhosted.org/packages/88/15/8856a0ab76708553ff597dd2e617b088c734ba87dc3fd395e2b2f3efffe8/cftime-1.6.5-cp312-cp312-win_amd64.whl", hash = "sha256:da84534c43699960dc980a9a765c33433c5de1a719a4916748c2d0e97a071e44", size = 464840, upload-time = "2025-10-13T18:56:12.506Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/85/451009a986d9273d2208fc0898aa00262275b5773259bf3f942f6716a9e7/cftime-1.6.5-cp312-cp312-win_arm64.whl", hash = "sha256:c62cd8db9ea40131eea7d4523691c5d806d3265d31279e4a58574a42c28acd77", size = 450534, upload-time = "2026-01-02T21:16:48.784Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/60/74ea344b3b003fada346ed98a6899085d6fd4c777df608992d90c458fda6/cftime-1.6.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4aba66fd6497711a47c656f3a732c2d1755ad15f80e323c44a8716ebde39ddd5", size = 502453, upload-time = "2025-10-13T18:56:13.545Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/14/adb293ac6127079b49ff11c05cf3d5ce5c1f17d097f326dc02d74ddfcb6e/cftime-1.6.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:89e7cba699242366e67d6fb5aee579440e791063f92a93853610c91647167c0d", size = 484541, upload-time = "2025-10-13T18:56:14.612Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/74/bb8a4566af8d0ef3f045d56c462a9115da4f04b07c7fbbf2b4875223eebd/cftime-1.6.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2f1eb43d7a7b919ec99aee709fb62ef87ef1cf0679829ef93d37cc1c725781e9", size = 1591014, upload-time = "2025-10-13T19:39:15.346Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/08/52f06ff2f04d376f9cd2c211aefcf2b37f1978e43289341f362fc99f6a0e/cftime-1.6.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e02a1d80ffc33fe469c7db68aa24c4a87f01da0c0c621373e5edadc92964900b", size = 1633625, upload-time = "2025-10-13T18:56:15.745Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/33/03e0b23d58ea8fab94ecb4f7c5b721e844a0800c13694876149d98830a73/cftime-1.6.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18ab754805233cdd889614b2b3b86a642f6d51a57a1ec327c48053f3414f87d8", size = 1684269, upload-time = "2025-10-13T18:56:17.04Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/60/a0cfba63847b43599ef1cdbbf682e61894994c22b9a79fd9e1e8c7e9de41/cftime-1.6.5-cp313-cp313-win_amd64.whl", hash = "sha256:6c27add8f907f4a4cd400e89438f2ea33e2eb5072541a157a4d013b7dbe93f9c", size = 465364, upload-time = "2025-10-13T18:56:18.05Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/e8/ec32f2aef22c15604e6fda39ff8d581a00b5469349f8fba61640d5358d2c/cftime-1.6.5-cp313-cp313-win_arm64.whl", hash = "sha256:31d1ff8f6bbd4ca209099d24459ec16dea4fb4c9ab740fbb66dd057ccbd9b1b9", size = 450468, upload-time = "2026-01-02T21:16:50.193Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/6c/a9618f589688358e279720f5c0fe67ef0077fba07334ce26895403ebc260/cftime-1.6.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c69ce3bdae6a322cbb44e9ebc20770d47748002fb9d68846a1e934f1bd5daf0b", size = 502725, upload-time = "2025-10-13T18:56:19.424Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/e3/da3c36398bfb730b96248d006cabaceed87e401ff56edafb2a978293e228/cftime-1.6.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e62e9f2943e014c5ef583245bf2e878398af131c97e64f8cd47c1d7baef5c4e2", size = 485445, upload-time = "2025-10-13T18:56:20.853Z" },
+ { url = "https://files.pythonhosted.org/packages/32/93/b05939e5abd14bd1ab69538bbe374b4ee2a15467b189ff895e9a8cdaddf6/cftime-1.6.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7da5fdaa4360d8cb89b71b8ded9314f2246aa34581e8105c94ad58d6102d9e4f", size = 1584434, upload-time = "2025-10-13T19:39:17.084Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/89/648397f9936e0b330999c4e776ebf296ec3c6a65f9901687dbca4ab820da/cftime-1.6.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bff865b4ea4304f2744a1ad2b8149b8328b321dd7a2b9746ef926d229bd7cd49", size = 1609812, upload-time = "2025-10-13T18:56:21.971Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/0f/901b4835aa67ad3e915605d4e01d0af80a44b114eefab74ae33de6d36933/cftime-1.6.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e552c5d1c8a58f25af7521e49237db7ca52ed2953e974fe9f7c4491e95fdd36c", size = 1669768, upload-time = "2025-10-13T18:56:24.027Z" },
+ { url = "https://files.pythonhosted.org/packages/22/d5/e605e4b28363e7a9ae98ed12cabbda5b155b6009270e6a231d8f10182a17/cftime-1.6.5-cp314-cp314-win_amd64.whl", hash = "sha256:e645b095dc50a38ac454b7e7f0742f639e7d7f6b108ad329358544a6ff8c9ba2", size = 463818, upload-time = "2025-10-13T18:56:25.376Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/89/a8f85ae697ff10206ec401c2621f5ca9f327554f586d62f244739ceeb347/cftime-1.6.5-cp314-cp314-win_arm64.whl", hash = "sha256:b9044d7ac82d3d8af189df1032fdc871bbd3f3dd41a6ec79edceb5029b71e6e0", size = 459862, upload-time = "2026-01-02T20:45:02.625Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/05/7410e12fd03a0c52717e74e6a1b49958810807dda212e23b65d43ea99676/cftime-1.6.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9ef56460cb0576e1a9161e1428c9e1a633f809a23fa9d598f313748c1ae5064e", size = 533781, upload-time = "2026-01-02T20:45:04.818Z" },
+ { url = "https://files.pythonhosted.org/packages/44/ba/10e3546426d3ed9f9cc82e4a99836bb6fac1642c7830f7bdd0ac1c3f0805/cftime-1.6.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4f4873d38b10032f9f3111c547a1d485519ae64eee6a7a2d091f1f8b08e1ba50", size = 515218, upload-time = "2026-01-02T20:45:06.788Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/68/efa11eae867749e921bfec6a865afdba8166e96188112dde70bb8bb49254/cftime-1.6.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ccce0f4c9d3f38dd948a117e578b50d0e0db11e2ca9435fb358fd524813e4b61", size = 1579932, upload-time = "2026-01-02T20:45:11.194Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/6c/0971e602c1390a423e6621dfbad9f1d375186bdaf9c9c7f75e06f1fbf355/cftime-1.6.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19cbfc5152fb0b34ce03acf9668229af388d7baa63a78f936239cb011ccbe6b1", size = 1555894, upload-time = "2026-01-02T20:45:16.351Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/fc/8475a15b7c3209a4a68b563dfc5e01ce74f2d8b9822372c3d30c68ab7f39/cftime-1.6.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4470cd5ef3c2514566f53efbcbb64dd924fa0584637d90285b2f983bd4ee7d97", size = 513027, upload-time = "2026-01-02T20:45:20.023Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/80/4ecbda8318fbf40ad4e005a4a93aebba69e81382e5b4c6086251cd5d0ee8/cftime-1.6.5-cp314-cp314t-win_arm64.whl", hash = "sha256:034c15a67144a0a5590ef150c99f844897618b148b87131ed34fda7072614662", size = 469065, upload-time = "2026-01-02T20:45:23.398Z" },
+]
+
[[package]]
name = "charset-normalizer"
version = "3.4.7"
@@ -634,6 +695,187 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a6/c4/0679472c60052c27efa612b4cd3ddd2a23e885dcdc73461781d2c802d39e/configobj-5.0.9-py2.py3-none-any.whl", hash = "sha256:1ba10c5b6ee16229c79a05047aeda2b55eb4e80d7c7d8ecf17ec1ca600c79882", size = 35615, upload-time = "2024-11-26T14:03:32.972Z" },
]
+[[package]]
+name = "contourpy"
+version = "1.3.2"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11' and sys_platform == 'darwin'",
+ "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "python_full_version < '3.11' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_machine != 'ARM64' and sys_platform == 'win32') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')",
+]
+dependencies = [
+ { name = "numpy", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" },
+ { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" },
+ { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" },
+ { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" },
+ { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" },
+ { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" },
+ { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" },
+ { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" },
+ { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" },
+ { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" },
+ { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" },
+ { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" },
+ { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" },
+ { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" },
+ { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" },
+ { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" },
+ { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" },
+ { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" },
+ { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" },
+ { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" },
+ { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" },
+ { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" },
+]
+
+[[package]]
+name = "contourpy"
+version = "1.3.3"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and sys_platform == 'emscripten'",
+ "python_full_version >= '3.14' and sys_platform == 'darwin'",
+ "python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
+ "python_full_version == '3.13.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and sys_platform == 'emscripten'",
+ "python_full_version == '3.12.*' and sys_platform == 'emscripten'",
+ "python_full_version == '3.13.*' and sys_platform == 'darwin'",
+ "python_full_version == '3.12.*' and sys_platform == 'darwin'",
+ "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
+ "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
+ "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and sys_platform == 'emscripten'",
+ "python_full_version == '3.11.*' and sys_platform == 'darwin'",
+ "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
+]
+dependencies = [
+ { name = "numpy", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" },
+ { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" },
+ { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" },
+ { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" },
+ { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" },
+ { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" },
+ { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" },
+ { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" },
+ { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" },
+ { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" },
+ { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" },
+ { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" },
+ { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" },
+ { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" },
+ { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" },
+ { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" },
+ { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" },
+ { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" },
+ { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" },
+ { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" },
+ { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" },
+ { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" },
+ { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" },
+ { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" },
+ { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" },
+ { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" },
+ { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" },
+ { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" },
+ { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" },
+ { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" },
+ { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" },
+ { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" },
+ { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" },
+]
+
[[package]]
name = "country-converter"
version = "1.3.2"
@@ -707,6 +949,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a2/ca/7e8365deec19afb2b2c7be7c1c0aa8f99633b54e90c570999acda93260fc/cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a", size = 3739536, upload-time = "2026-05-04T22:59:29.61Z" },
]
+[[package]]
+name = "cycler"
+version = "0.12.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
+]
+
[[package]]
name = "dask"
version = "2026.3.0"
@@ -833,6 +1084,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/51/ac/e5d886f892666d2d1e5cb8c1a41146e1d79ae8896477b1153a21711d3b44/fasteners-0.20-py3-none-any.whl", hash = "sha256:9422c40d1e350e4259f509fb2e608d6bc43c0136f79a00db1b49046029d0b3b7", size = 18702, upload-time = "2025-08-11T10:19:35.716Z" },
]
+[[package]]
+name = "filelock"
+version = "3.29.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/91/f5/3557bf28e0f1943e4849154c821533706e6dea010f96fb6aa0b6949037d1/filelock-3.29.3.tar.gz", hash = "sha256:7fc1b3f39cf172fd8203812043c57b8a65aef9969f38b6704f628b881f761a84", size = 61956, upload-time = "2026-06-10T17:37:11.832Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/81/8f/b61d427c4f49a8bdadc93f4e7e74df8a6df6f77ee6e26bf0df53d3925363/filelock-3.29.3-py3-none-any.whl", hash = "sha256:e58333029cc9b925f39aad59b1d8f0a1ad836af4e60d7217f4a4dba87461261d", size = 42324, upload-time = "2026-06-10T17:37:10.37Z" },
+]
+
[[package]]
name = "fiona"
version = "1.10.1"
@@ -872,6 +1132,63 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" },
]
+[[package]]
+name = "fonttools"
+version = "4.63.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/84/69/c97f2c18e0db87d2c7b15da1974dace76ae938f1cfa22e2727a648b7ed43/fonttools-4.63.0.tar.gz", hash = "sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0", size = 3597189, upload-time = "2026-05-14T12:04:30.958Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f2/c9/4141c90a90db20f807c7e10bfd689fe53eb8f7f4caff58ee4d4dfe46919f/fonttools-4.63.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e3297a6a4059b4acc3a1e9a8b04741f240a80044eef08ebd32e8b5bcdddce75b", size = 2884632, upload-time = "2026-05-14T12:02:38.56Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/46/ad12b5c10eae602d7ef814b02afa08aacbf89da917fed5b071282b7eadc2/fonttools-4.63.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b1cd75a03ad8cb5bc40c90bfde68c0c47de423aa19e5c0f362b43520645eea94", size = 2429441, upload-time = "2026-05-14T12:02:41.162Z" },
+ { url = "https://files.pythonhosted.org/packages/90/8f/bdca24a84c81d56fffed052229cdcff368f6e05882e526f4558891481f65/fonttools-4.63.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0425b277a59cff3d80ca42162a8de360f318438a2ac83570842a678d826d579", size = 4946346, upload-time = "2026-05-14T12:02:43.41Z" },
+ { url = "https://files.pythonhosted.org/packages/04/59/a639c0e136441ee91a65b56fdf89e5d075927e7a09c559d1b0f5276577db/fonttools-4.63.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d7e5c9973aa04c95650c96e5f5ad865fbf42d62079163ecfab1e01cbc2504c22", size = 4903184, upload-time = "2026-05-14T12:02:45.742Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/53/91b7e0cb45b536f3da1b29ba8cbab89f27e8b986809e0b1982303a3f4eca/fonttools-4.63.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cb014d58140a38135f16064c74c652ed57aa0b75cbf8bb59cac821f7edb5334e", size = 4922967, upload-time = "2026-05-14T12:02:48.386Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/b7/87439bf44e6b97c5538cd29d0b7e366a5b8ce2cc132a4134fb67fa3f2fa2/fonttools-4.63.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:032038247a96c1690f9f31e377c389383c902531b085aa4e4dabd6f57f870e69", size = 5042799, upload-time = "2026-05-14T12:02:50.424Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/7c/8b96c3263b89ef99cded544c0f0636686f85dbd3c211c4dceef0231fca23/fonttools-4.63.0-cp310-cp310-win32.whl", hash = "sha256:a8b33a82979e0a6a34ff435cc81317be1f95ec1ebb7a3a2d1c8a6a54f02ae44e", size = 1519704, upload-time = "2026-05-14T12:02:52.523Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/4d/2c2f0069970b6907de8fb5b05c5c0193cc22f717df151d1c7aef1c738f58/fonttools-4.63.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c18358a155d75034911c5ee397a5b44cd19dd325dbb8b35fb60bf421d6a72ac", size = 1568666, upload-time = "2026-05-14T12:02:54.917Z" },
+ { url = "https://files.pythonhosted.org/packages/75/2b/a7f1545bdf5da69c4bda0cea2a5781f0ad2a6623e0277267672db43c5fe6/fonttools-4.63.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b8ae05d9eacf6081414d759c0a352769ac28ce31280d6bb8e77b03f9e3c449f", size = 2881793, upload-time = "2026-05-14T12:02:56.645Z" },
+ { url = "https://files.pythonhosted.org/packages/49/50/965308c703f085f225db2886813b27e015b8b3438c350b22dd65b52c2a2c/fonttools-4.63.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79cdc9f567aec74a72918fd060283911406750cbc9fd28c1316023deb6ce31a9", size = 2428130, upload-time = "2026-05-14T12:02:58.891Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/38/6937fbd7f2dc3a6b48725851bc2c15ec949b9af14d9bbcb5fe83cdf9bdf9/fonttools-4.63.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c14b4fd138c4bafcca294765c547914e1aa431ae1ca94ab99d8db08c958bd3b", size = 5111952, upload-time = "2026-05-14T12:03:01.263Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/43/a81f20050a3115b57d62c8e781446949512eac36690dc384ccea65ff4cc1/fonttools-4.63.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76ac49f929aecaf82d83250b8347e099d7aecba0f4726c1d9b6df3b8bb5fe18", size = 5082308, upload-time = "2026-05-14T12:03:03.211Z" },
+ { url = "https://files.pythonhosted.org/packages/67/00/cdd9d4944ca6ae280d01e69cc37bde3bf663630b837a6fc6d2cd65d80e0e/fonttools-4.63.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dcf076a4474fe0d7367e5bbf5b052c7284fa1feca729c04176ce513521afd8a0", size = 5087932, upload-time = "2026-05-14T12:03:05.147Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/f1/0aa0dbea778c75adbef223c42019fd47d22262b905974d62d829545d485f/fonttools-4.63.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7dd683fef0663e9f0f45cf541d788d24caa3ec9db50796b588e1757d8b3bc007", size = 5213271, upload-time = "2026-05-14T12:03:07.238Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/99/253e4056e1f0e67b9390125a154b73b5eb73ad521bece95c004858fdeec2/fonttools-4.63.0-cp311-cp311-win32.whl", hash = "sha256:afefc1ed0a59785a7fb06ea7e1678e849c193e1e387db783579bc7b3056fcfcb", size = 2304473, upload-time = "2026-05-14T12:03:09.271Z" },
+ { url = "https://files.pythonhosted.org/packages/08/60/defa5e69641db890a63be281f41345f4c33b157824eaf0b9fad3e08b0dcb/fonttools-4.63.0-cp311-cp311-win_amd64.whl", hash = "sha256:063e08bd17bd5a90127a14123de0d6a952dbc847695fd98b63c043d58057f90c", size = 2356389, upload-time = "2026-05-14T12:03:11.53Z" },
+ { url = "https://files.pythonhosted.org/packages/08/ef/b3c6b9b5be2f82416d73fe2ed2e96e2793cd80e7510bd6a17ca79cdd88ec/fonttools-4.63.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:37dd23e621e3b0aef1baa70a303b80aaf38449632cfc8fd2a55fb285bbccfc02", size = 2881131, upload-time = "2026-05-14T12:03:13.386Z" },
+ { url = "https://files.pythonhosted.org/packages/44/a0/c815bea63117fa63e4e1c01f8a1110d2112fa003f838e6467094ec2432ce/fonttools-4.63.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9faff9e0c1f76f9fd55899d2ce785832efebab37eb8ae13995853aef178bef0", size = 2426704, upload-time = "2026-05-14T12:03:15.801Z" },
+ { url = "https://files.pythonhosted.org/packages/44/04/0b91d8e916e92ad1fac9e4624760baf0fd5ff2ead614c2f68fb21373f03f/fonttools-4.63.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef3048ef05dbb552b89817713d9cac912e00d0fde4a3105c00d29e52e10c89af", size = 5044298, upload-time = "2026-05-14T12:03:18.085Z" },
+ { url = "https://files.pythonhosted.org/packages/77/c7/2342da9830e3e9d4870305ca5d2091d2a83284f2953079b7bdd3b5e029d8/fonttools-4.63.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58dc6bb86a78d782f00f9190ca02c119cf5bbe2807536e361e18d42019f877d8", size = 4999800, upload-time = "2026-05-14T12:03:20.161Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/6d/67fe16c48d7ce050979b33f47e0d28a318f02da030602e944c34f7a16ef3/fonttools-4.63.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee08ebfa58f6e1aeff5697ab9582105bb620008c1caafb681e4c557e7483027b", size = 4982666, upload-time = "2026-05-14T12:03:22.87Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/00/3bbab338c07c71fa56269953845e92c951a61457bbbb0f1022551ea266d9/fonttools-4.63.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:27fdc65af8da6f88b9c6121c47a464cbe359fcfff7ff6fc2d37a1f395d755b78", size = 5133598, upload-time = "2026-05-14T12:03:25.168Z" },
+ { url = "https://files.pythonhosted.org/packages/62/f2/aa27c7f98db5b064883dadcc5283947e81e034de42e22a33675878d98b54/fonttools-4.63.0-cp312-cp312-win32.whl", hash = "sha256:af2fd1664d00a397d75f806985ddb36282091c2131a73a6485c23b4a34722263", size = 2292575, upload-time = "2026-05-14T12:03:27.496Z" },
+ { url = "https://files.pythonhosted.org/packages/87/36/cccb9bc2a6ab63d1b2980374f0dca72ce95ae267c9b4cfe77455bb70d0d4/fonttools-4.63.0-cp312-cp312-win_amd64.whl", hash = "sha256:59ac449f8cca9b4ffa08d2e7bbadad87ce710d69d1eda5c3c1ce579baa987272", size = 2343211, upload-time = "2026-05-14T12:03:30.057Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/8d/d8fec3dcde2963f8c908fb315e5ff2cd0ac34f82394bbbf73a2aa5145ce3/fonttools-4.63.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd7e9857e5e63738b9d9fd707bc1f59c8b09e5177726d23664db393c59bb08bd", size = 2876062, upload-time = "2026-05-14T12:03:32.554Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/71/d935dc54e4ff121bfdd11e08702db63a7e6f25af21d8a3d7b7212df53641/fonttools-4.63.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c2a2a42198b696a6f48fad91709afb55176e66a5e566131219dba372fb7f8c59", size = 2424594, upload-time = "2026-05-14T12:03:34.86Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/40/e76320afa1df918e146155ef239b1719ee266092e96f5423bfd075affba1/fonttools-4.63.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e874792a8212b44583ea02189d9e693906b2f78b261f372f95d6c563210ac1d", size = 5024840, upload-time = "2026-05-14T12:03:36.745Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/36/0b805d8c485f872f65a509cbe3b58a5d0d17bee855333b54a150c79d3061/fonttools-4.63.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22135da48a348785c5e2d5d2d9d6bec5ed44adacbaeb9db12d9493bf6c6bfa68", size = 4975801, upload-time = "2026-05-14T12:03:38.833Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/26/2cee03d0aa083ab022da5c07aff9ed3f689da1defb81ad6917c9627896da/fonttools-4.63.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ccf41f2efdf56994d22d73bef4ced1052161958169428d06ba9724ea9e9a64be", size = 4965009, upload-time = "2026-05-14T12:03:41.494Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/48/cc4b66d9058c0d0982c833fad10127c4b0e9324606aafa41382295ca4102/fonttools-4.63.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9ced0bd02ac751dd6319b0da88aaef24414e3b0dbc32bb4f24944821a3741a27", size = 5105892, upload-time = "2026-05-14T12:03:43.525Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/1f/a98a30a814b9ddef3a2e706025f90b9e0bc94890e6cb15254bc86547d11a/fonttools-4.63.0-cp313-cp313-win32.whl", hash = "sha256:85be818f5506e8a7753153def2c9550178f0ecae6a47b5e0e8dbb23f7cc90380", size = 2291313, upload-time = "2026-05-14T12:03:45.594Z" },
+ { url = "https://files.pythonhosted.org/packages/92/46/5177b01f3b4abfdd4409f31cca4ab279c9343a26efbe9ec78c97fc612e02/fonttools-4.63.0-cp313-cp313-win_amd64.whl", hash = "sha256:ba04cb5891d4c0c21b6da95eda8d7b090021508a294fff33464fc7d241e0856b", size = 2342299, upload-time = "2026-05-14T12:03:47.414Z" },
+ { url = "https://files.pythonhosted.org/packages/27/d2/23d25e3f247b328be58d04a4c9f894178a0d1eda7d42867cfb388adaf416/fonttools-4.63.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fd1e3094f42d806d3d7c79162fc59e5910fcbe3a7360c385b8da969bc4493745", size = 2875338, upload-time = "2026-05-14T12:03:50.052Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/58/7dfa0c761cb3b2964e2a84c4dc986c926a87de0cb9fb60d5b28ded3f2914/fonttools-4.63.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6e528da43bc3791085f8cb6141b1d13e459226790240340fcbb4625649238b03", size = 2422661, upload-time = "2026-05-14T12:03:52.154Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/87/64cfa18a7a1621d17b7f4502b2b0ed8a135a90c3db51ea590ee99043e76b/fonttools-4.63.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b2248c5decb223562f7902ff6325077a073f608ee8e33e88ad88db734eb9f49", size = 5010526, upload-time = "2026-05-14T12:03:54.647Z" },
+ { url = "https://files.pythonhosted.org/packages/36/e1/a8933a72c45a87177fbde2696e0d0755c8c9062f8c077a961c6215fa27b1/fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:308f957cdeaf8abe4e5f2f124902ef405448af92c90f80e302a3b771c2e6116b", size = 4923946, upload-time = "2026-05-14T12:03:56.984Z" },
+ { url = "https://files.pythonhosted.org/packages/27/60/872e6e233b8c5e8b41413796ff18b7fe479661bd40147e071b450dfad7a1/fonttools-4.63.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bf00f21eb5fb721dbaf73d1e9da6d02a1af7768f2ebcf9798be98beab8ba90f6", size = 4962489, upload-time = "2026-05-14T12:03:59.443Z" },
+ { url = "https://files.pythonhosted.org/packages/30/c4/83c24f2ec38b90cfda84bf4b1a1f49df80e84a1db4e7ac6e0d41bf23bc39/fonttools-4.63.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c1aaa4b9c75798400ac043ce04d74e7830376c85095a5a6ed7cba2f17a266bf4", size = 5071870, upload-time = "2026-05-14T12:04:02.122Z" },
+ { url = "https://files.pythonhosted.org/packages/de/40/3ae22b60ff1d41ce0bd044b31238cdc72cef99f28b976f1e128ebd618c9b/fonttools-4.63.0-cp314-cp314-win32.whl", hash = "sha256:22693918177bd9ceabec4736d338045f357769416fc6b0b2508eefef75b08616", size = 2295026, upload-time = "2026-05-14T12:04:04.47Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/d4/98078064ccc76b45cb0f6c002452011e93c4bd26f6850344f0951cc1fe89/fonttools-4.63.0-cp314-cp314-win_amd64.whl", hash = "sha256:7d782fac32985914c351556f68ac0855391572bcd87de50e05970d3cd4c96fc5", size = 2347454, upload-time = "2026-05-14T12:04:06.752Z" },
+ { url = "https://files.pythonhosted.org/packages/49/4e/652d1580c5f4e39f7d103b0c793e4773129ad633dce4addd0cf4dfebde02/fonttools-4.63.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6db5140a60a5d731d21ec076745b40a310607731b0a565b50776393188649001", size = 2958152, upload-time = "2026-05-14T12:04:08.706Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/55/ad864c9a9b219f552eb46b32cd7906c466e5a578ba0c3abfcc0fe7413eb6/fonttools-4.63.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d76edbff9014094dbf03bd2d074709dfa6ec7aba13d838c937a2b33d2d6a86e", size = 2460809, upload-time = "2026-05-14T12:04:10.783Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/2b/0aa8db70f18cf52e49b4ed5ecec68547f981160bf5ded3b5aed6faa0a6f9/fonttools-4.63.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0eac00b9118c3c2f87d272e45341871c5b3066baa3c86897fa634a7c3fb59096", size = 5148649, upload-time = "2026-05-14T12:04:12.747Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/63/18e4369c25043096f1048e0c9915951adc4f842bd81c6b18155824d6fa99/fonttools-4.63.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51394295f1a51de8b5f30bdb1e1b9a4231536c7064ef5c6e211eec19fa36036f", size = 4932147, upload-time = "2026-05-14T12:04:14.806Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/3f/67f3eac2ffd8a98446c5022f8ed3864eac878a5ff7af8df4c8286dba16cc/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9e12f105d2b6342c559c298afb674006bb2893afc7102dcf8a1b55b0486b4e40", size = 5027237, upload-time = "2026-05-14T12:04:17.675Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/ba/4e6214cb38a7b04779e97bb7636de9a5c7f20af7018d03dee0b64c08510a/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:796f27556dbe094c4824f75ca85267e4df776c79036c8441469a4df37038c196", size = 5053933, upload-time = "2026-05-14T12:04:20.818Z" },
+ { url = "https://files.pythonhosted.org/packages/34/3b/214dcc19ee31d3d38fb5ad2755c11ef0514e5dc300bbaf41c0b69f393799/fonttools-4.63.0-cp314-cp314t-win32.whl", hash = "sha256:948428a275741f0b64b113c955425a953314f4b9ab9997f73a72c83e68e569c8", size = 2359326, upload-time = "2026-05-14T12:04:24.22Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/1e/3ff1a9b523058c2eeb6a9d50f5574e2a738200d0d94107d5bc4105e8da3f/fonttools-4.63.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6d4741eb179121cab9eea4cb2393d24492373a260d7945006358c08cfbf45419", size = 2425829, upload-time = "2026-05-14T12:04:26.829Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" },
+]
+
[[package]]
name = "frozenlist"
version = "1.8.0"
@@ -1070,7 +1387,7 @@ name = "google-pasta"
version = "0.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "six" },
+ { name = "six", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/35/4a/0bd53b36ff0323d10d5f24ebd67af2de10a1117f5cf4d7add90df92756f1/google-pasta-0.2.0.tar.gz", hash = "sha256:c9f2c8dfc8f96d0d5808299920721be30c9eec37f2389f28904f454565c8a16e", size = 40430, upload-time = "2020-03-13T18:57:50.34Z" }
wheels = [
@@ -1148,7 +1465,7 @@ name = "grpcio"
version = "1.80.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "typing-extensions" },
+ { name = "typing-extensions", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b7/48/af6173dbca4454f4637a4678b67f52ca7e0c1ed7d5894d89d434fecede05/grpcio-1.80.0.tar.gz", hash = "sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257", size = 12978905, upload-time = "2026-03-30T08:49:10.502Z" }
wheels = [
@@ -1238,6 +1555,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/95/5f/ee7d49f219522a9235152cfd5968c9ca13cb7c15e9b827b39fb7640aed8a/h3-4.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:7767f82d383f4e605b9e79690ddcfaf6264edbf9046396117fdd7ee74473c839", size = 898314, upload-time = "2026-01-29T19:22:41.423Z" },
]
+[[package]]
+name = "h5netcdf"
+version = "1.8.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+ { name = "packaging" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ef/03/92d6cc02c0055158167255980461155d6e17f1c4143c03f8bcc18d3e3f3a/h5netcdf-1.8.1.tar.gz", hash = "sha256:9b396a4cc346050fc1a4df8523bc1853681ec3544e0449027ae397cb953c7a16", size = 78679, upload-time = "2026-01-23T07:35:31.233Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b1/8b/88f16936a8e8070a83d36239555227ecd91728f9ef222c5382cda07e0fd6/h5netcdf-1.8.1-py3-none-any.whl", hash = "sha256:a76ed7cfc9b8a8908ea7057c4e57e27307acff1049b7f5ed52db6c2247636879", size = 62915, upload-time = "2026-01-23T07:35:30.195Z" },
+]
+
[[package]]
name = "h5py"
version = "3.16.0"
@@ -1397,17 +1727,17 @@ source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.11' and sys_platform == 'darwin'",
"python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')",
+ "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_machine != 'ARM64' and sys_platform == 'win32') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')",
]
dependencies = [
- { name = "absl-py", marker = "python_full_version < '3.11'" },
- { name = "h5py", marker = "python_full_version < '3.11'" },
- { name = "ml-dtypes", marker = "python_full_version < '3.11'" },
- { name = "namex", marker = "python_full_version < '3.11'" },
- { name = "numpy", marker = "python_full_version < '3.11'" },
- { name = "optree", marker = "python_full_version < '3.11'" },
- { name = "packaging", marker = "python_full_version < '3.11'" },
- { name = "rich", marker = "python_full_version < '3.11'" },
+ { name = "absl-py", marker = "(python_full_version < '3.11' and platform_machine != 'ARM64') or (python_full_version < '3.11' and sys_platform != 'win32')" },
+ { name = "h5py", marker = "(python_full_version < '3.11' and platform_machine != 'ARM64') or (python_full_version < '3.11' and sys_platform != 'win32')" },
+ { name = "ml-dtypes", marker = "(python_full_version < '3.11' and platform_machine != 'ARM64') or (python_full_version < '3.11' and sys_platform != 'win32')" },
+ { name = "namex", marker = "(python_full_version < '3.11' and platform_machine != 'ARM64') or (python_full_version < '3.11' and sys_platform != 'win32')" },
+ { name = "numpy", marker = "(python_full_version < '3.11' and platform_machine != 'ARM64') or (python_full_version < '3.11' and sys_platform != 'win32')" },
+ { name = "optree", marker = "(python_full_version < '3.11' and platform_machine != 'ARM64') or (python_full_version < '3.11' and sys_platform != 'win32')" },
+ { name = "packaging", marker = "(python_full_version < '3.11' and platform_machine != 'ARM64') or (python_full_version < '3.11' and sys_platform != 'win32')" },
+ { name = "rich", marker = "(python_full_version < '3.11' and platform_machine != 'ARM64') or (python_full_version < '3.11' and sys_platform != 'win32')" },
]
sdist = { url = "https://files.pythonhosted.org/packages/63/73/19e057f7a2a6d641246bacca21e0bbcb2be341afca98ea461a0f2a9ab92d/keras-3.12.2.tar.gz", hash = "sha256:e19c7c7f8f2a81e44d4f203e567731a15a270d8ef351060982b45a1fafdf3fce", size = 1129833, upload-time = "2026-05-07T21:48:18.396Z" }
wheels = [
@@ -1419,13 +1749,13 @@ name = "keras"
version = "3.14.1"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
- "python_full_version >= '3.14' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
"python_full_version >= '3.14' and sys_platform == 'darwin'",
"python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
- "python_full_version == '3.13.*' and sys_platform == 'win32'",
- "python_full_version == '3.12.*' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version == '3.13.*' and sys_platform == 'emscripten'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten'",
"python_full_version == '3.13.*' and sys_platform == 'darwin'",
@@ -1434,27 +1764,151 @@ resolution-markers = [
"python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
"(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
- "python_full_version == '3.11.*' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version == '3.11.*' and sys_platform == 'emscripten'",
"python_full_version == '3.11.*' and sys_platform == 'darwin'",
"python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
]
dependencies = [
- { name = "absl-py", marker = "python_full_version >= '3.11'" },
- { name = "h5py", marker = "python_full_version >= '3.11'" },
- { name = "ml-dtypes", marker = "python_full_version >= '3.11'" },
- { name = "namex", marker = "python_full_version >= '3.11'" },
- { name = "numpy", marker = "python_full_version >= '3.11'" },
- { name = "optree", marker = "python_full_version >= '3.11'" },
- { name = "packaging", marker = "python_full_version >= '3.11'" },
- { name = "rich", marker = "python_full_version >= '3.11'" },
+ { name = "absl-py", marker = "(python_full_version >= '3.11' and platform_machine != 'ARM64') or (python_full_version >= '3.11' and sys_platform != 'win32')" },
+ { name = "h5py", marker = "(python_full_version >= '3.11' and platform_machine != 'ARM64') or (python_full_version >= '3.11' and sys_platform != 'win32')" },
+ { name = "ml-dtypes", marker = "(python_full_version >= '3.11' and platform_machine != 'ARM64') or (python_full_version >= '3.11' and sys_platform != 'win32')" },
+ { name = "namex", marker = "(python_full_version >= '3.11' and platform_machine != 'ARM64') or (python_full_version >= '3.11' and sys_platform != 'win32')" },
+ { name = "numpy", marker = "(python_full_version >= '3.11' and platform_machine != 'ARM64') or (python_full_version >= '3.11' and sys_platform != 'win32')" },
+ { name = "optree", marker = "(python_full_version >= '3.11' and platform_machine != 'ARM64') or (python_full_version >= '3.11' and sys_platform != 'win32')" },
+ { name = "packaging", marker = "(python_full_version >= '3.11' and platform_machine != 'ARM64') or (python_full_version >= '3.11' and sys_platform != 'win32')" },
+ { name = "rich", marker = "(python_full_version >= '3.11' and platform_machine != 'ARM64') or (python_full_version >= '3.11' and sys_platform != 'win32')" },
]
sdist = { url = "https://files.pythonhosted.org/packages/35/e7/97a7664581b73e4f9ff1d3a767a493b6ac5d3e0ed1926bd2b6b2c8bbccd7/keras-3.14.1.tar.gz", hash = "sha256:ef479173102ad29db89b53c232efdc3fb5ad57c28bc27ead59f3e78a1eecd05b", size = 1263647, upload-time = "2026-05-07T21:43:35.112Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/02/03/184267c1d09783dd070f1ddfd0d4beb7503139dfc7bd75b422867cf282fd/keras-3.14.1-py3-none-any.whl", hash = "sha256:ebd2c14d2af3c9de18083604d408483996407fc7d2f9ebd1d565961f96608c29", size = 1628606, upload-time = "2026-05-07T21:43:32.737Z" },
]
+[[package]]
+name = "kiwisolver"
+version = "1.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ac/f8/06549565caa026e540b7e7bab5c5a90eb7ca986015f4c48dace243cd24d9/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32cc0a5365239a6ea0c6ed461e8838d053b57e397443c0ca894dcc8e388d4374", size = 122802, upload-time = "2026-03-09T13:12:37.515Z" },
+ { url = "https://files.pythonhosted.org/packages/84/eb/8476a0818850c563ff343ea7c9c05dcdcbd689a38e01aa31657df01f91fa/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cc0b66c1eec9021353a4b4483afb12dfd50e3669ffbb9152d6842eb34c7e29fd", size = 66216, upload-time = "2026-03-09T13:12:38.812Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/c4/f9c8a6b4c21aed4198566e45923512986d6cef530e7263b3a5f823546561/kiwisolver-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86e0287879f75621ae85197b0877ed2f8b7aa57b511c7331dce2eb6f4de7d476", size = 63917, upload-time = "2026-03-09T13:12:40.053Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/0e/ba4ae25d03722f64de8b2c13e80d82ab537a06b30fc7065183c6439357e3/kiwisolver-1.5.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:62f59da443c4f4849f73a51a193b1d9d258dcad0c41bc4d1b8fb2bcc04bfeb22", size = 1628776, upload-time = "2026-03-09T13:12:41.976Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/e4/3f43a011bc8a0860d1c96f84d32fa87439d3feedf66e672fef03bf5e8bac/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9190426b7aa26c5229501fa297b8d0653cfd3f5a36f7990c264e157cbf886b3b", size = 1228164, upload-time = "2026-03-09T13:12:44.002Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/34/3a901559a1e0c218404f9a61a93be82d45cb8f44453ba43088644980f033/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c8277104ded0a51e699c8c3aff63ce2c56d4ed5519a5f73e0fd7057f959a2b9e", size = 1246656, upload-time = "2026-03-09T13:12:45.557Z" },
+ { url = "https://files.pythonhosted.org/packages/87/9e/f78c466ea20527822b95ad38f141f2de1dcd7f23fb8716b002b0d91bbe59/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8f9baf6f0a6e7571c45c8863010b45e837c3ee1c2c77fcd6ef423be91b21fedb", size = 1295562, upload-time = "2026-03-09T13:12:47.562Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/66/fd0e4a612e3a286c24e6d6f3a5428d11258ed1909bc530ba3b59807fd980/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cff8e5383db4989311f99e814feeb90c4723eb4edca425b9d5d9c3fefcdd9537", size = 2178473, upload-time = "2026-03-09T13:12:50.254Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/8e/6cac929e0049539e5ee25c1ee937556f379ba5204840d03008363ced662d/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ebae99ed6764f2b5771c522477b311be313e8841d2e0376db2b10922daebbba4", size = 2274035, upload-time = "2026-03-09T13:12:51.785Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/d3/9d0c18f1b52ea8074b792452cf17f1f5a56bd0302a85191f405cfbf9da16/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d5cd5189fc2b6a538b75ae45433140c4823463918f7b1617c31e68b085c0022c", size = 2443217, upload-time = "2026-03-09T13:12:53.329Z" },
+ { url = "https://files.pythonhosted.org/packages/45/2a/6e19368803a038b2a90857bf4ee9e3c7b667216d045866bf22d3439fd75e/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f42c23db5d1521218a3276bb08666dcb662896a0be7347cba864eca45ff64ede", size = 2249196, upload-time = "2026-03-09T13:12:55.057Z" },
+ { url = "https://files.pythonhosted.org/packages/75/2b/3f641dfcbe72e222175d626bacf2f72c3b34312afec949dd1c50afa400f5/kiwisolver-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:94eff26096eb5395136634622515b234ecb6c9979824c1f5004c6e3c3c85ccd2", size = 73389, upload-time = "2026-03-09T13:12:56.496Z" },
+ { url = "https://files.pythonhosted.org/packages/da/88/299b137b9e0025d8982e03d2d52c123b0a2b159e84b0ef1501ef446339cf/kiwisolver-1.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:dd952e03bfbb096cfe2dd35cd9e00f269969b67536cb4370994afc20ff2d0875", size = 64782, upload-time = "2026-03-09T13:12:57.609Z" },
+ { url = "https://files.pythonhosted.org/packages/12/dd/a495a9c104be1c476f0386e714252caf2b7eca883915422a64c50b88c6f5/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9eed0f7edbb274413b6ee781cca50541c8c0facd3d6fd289779e494340a2b85c", size = 122798, upload-time = "2026-03-09T13:12:58.963Z" },
+ { url = "https://files.pythonhosted.org/packages/11/60/37b4047a2af0cf5ef6d8b4b26e91829ae6fc6a2d1f74524bcb0e7cd28a32/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c4923e404d6bcd91b6779c009542e5647fef32e4a5d75e115e3bbac6f2335eb", size = 66216, upload-time = "2026-03-09T13:13:00.155Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/aa/510dc933d87767584abfe03efa445889996c70c2990f6f87c3ebaa0a18c5/kiwisolver-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac", size = 63911, upload-time = "2026-03-09T13:13:01.671Z" },
+ { url = "https://files.pythonhosted.org/packages/80/46/bddc13df6c2a40741e0cc7865bb1c9ed4796b6760bd04ce5fae3928ef917/kiwisolver-1.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27", size = 1438209, upload-time = "2026-03-09T13:13:03.385Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/d6/76621246f5165e5372f02f5e6f3f48ea336a8f9e96e43997d45b240ed8cd/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff710414307fefa903e0d9bdf300972f892c23477829f49504e59834f4195398", size = 1248888, upload-time = "2026-03-09T13:13:05.231Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/c1/31559ec6fb39a5b48035ce29bb63ade628f321785f38c384dee3e2c08bc1/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6176c1811d9d5a04fa391c490cc44f451e240697a16977f11c6f722efb9041db", size = 1266304, upload-time = "2026-03-09T13:13:06.743Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/ef/1cb8276f2d29cc6a41e0a042f27946ca347d3a4a75acf85d0a16aa6dcc82/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50847dca5d197fcbd389c805aa1a1cf32f25d2e7273dc47ab181a517666b68cc", size = 1319650, upload-time = "2026-03-09T13:13:08.607Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/e4/5ba3cecd7ce6236ae4a80f67e5d5531287337d0e1f076ca87a5abe4cd5d0/kiwisolver-1.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:01808c6d15f4c3e8559595d6d1fe6411c68e4a3822b4b9972b44473b24f4e679", size = 970949, upload-time = "2026-03-09T13:13:10.299Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/69/dc61f7ae9a2f071f26004ced87f078235b5507ab6e5acd78f40365655034/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1f9f4121ec58628c96baa3de1a55a4e3a333c5102c8e94b64e23bf7b2083309", size = 2199125, upload-time = "2026-03-09T13:13:11.841Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/7b/abbe0f1b5afa85f8d084b73e90e5f801c0939eba16ac2e49af7c61a6c28d/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b7d335370ae48a780c6e6a6bbfa97342f563744c39c35562f3f367665f5c1de2", size = 2293783, upload-time = "2026-03-09T13:13:14.399Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/80/5908ae149d96d81580d604c7f8aefd0e98f4fd728cf172f477e9f2a81744/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:800ee55980c18545af444d93fdd60c56b580db5cc54867d8cbf8a1dc0829938c", size = 1960726, upload-time = "2026-03-09T13:13:16.047Z" },
+ { url = "https://files.pythonhosted.org/packages/84/08/a78cb776f8c085b7143142ce479859cfec086bd09ee638a317040b6ef420/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c438f6ca858697c9ab67eb28246c92508af972e114cac34e57a6d4ba17a3ac08", size = 2464738, upload-time = "2026-03-09T13:13:17.897Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/e1/65584da5356ed6cb12c63791a10b208860ac40a83de165cb6a6751a686e3/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c63c91f95173f9c2a67c7c526b2cea976828a0e7fced9cdcead2802dc10f8a4", size = 2270718, upload-time = "2026-03-09T13:13:19.421Z" },
+ { url = "https://files.pythonhosted.org/packages/be/6c/28f17390b62b8f2f520e2915095b3c94d88681ecf0041e75389d9667f202/kiwisolver-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b", size = 73480, upload-time = "2026-03-09T13:13:20.818Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/0e/2ee5debc4f77a625778fec5501ff3e8036fe361b7ee28ae402a485bb9694/kiwisolver-1.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:ad4ae4ffd1ee9cd11357b4c66b612da9888f4f4daf2f36995eda64bd45370cac", size = 64930, upload-time = "2026-03-09T13:13:21.997Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/b2/818b74ebea34dabe6d0c51cb1c572e046730e64844da6ed646d5298c40ce/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9", size = 123158, upload-time = "2026-03-09T13:13:23.127Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/d9/405320f8077e8e1c5c4bd6adc45e1e6edf6d727b6da7f2e2533cf58bff71/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588", size = 66388, upload-time = "2026-03-09T13:13:24.765Z" },
+ { url = "https://files.pythonhosted.org/packages/99/9f/795fedf35634f746151ca8839d05681ceb6287fbed6cc1c9bf235f7887c2/kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819", size = 64068, upload-time = "2026-03-09T13:13:25.878Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/13/680c54afe3e65767bed7ec1a15571e1a2f1257128733851ade24abcefbcc/kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f", size = 1477934, upload-time = "2026-03-09T13:13:27.166Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/2f/cebfcdb60fd6a9b0f6b47a9337198bcbad6fbe15e68189b7011fd914911f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf", size = 1278537, upload-time = "2026-03-09T13:13:28.707Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/0d/9b782923aada3fafb1d6b84e13121954515c669b18af0c26e7d21f579855/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d", size = 1296685, upload-time = "2026-03-09T13:13:30.528Z" },
+ { url = "https://files.pythonhosted.org/packages/27/70/83241b6634b04fe44e892688d5208332bde130f38e610c0418f9ede47ded/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083", size = 1346024, upload-time = "2026-03-09T13:13:32.818Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/db/30ed226fb271ae1a6431fc0fe0edffb2efe23cadb01e798caeb9f2ceae8f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6", size = 987241, upload-time = "2026-03-09T13:13:34.435Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/bd/c314595208e4c9587652d50959ead9e461995389664e490f4dce7ff0f782/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1", size = 2227742, upload-time = "2026-03-09T13:13:36.4Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/43/0499cec932d935229b5543d073c2b87c9c22846aab48881e9d8d6e742a2d/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0", size = 2323966, upload-time = "2026-03-09T13:13:38.204Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/6f/79b0d760907965acfd9d61826a3d41f8f093c538f55cd2633d3f0db269f6/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15", size = 1977417, upload-time = "2026-03-09T13:13:39.966Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/31/01d0537c41cb75a551a438c3c7a80d0c60d60b81f694dac83dd436aec0d0/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314", size = 2491238, upload-time = "2026-03-09T13:13:41.698Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/34/8aefdd0be9cfd00a44509251ba864f5caf2991e36772e61c408007e7f417/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9", size = 2294947, upload-time = "2026-03-09T13:13:43.343Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/cf/0348374369ca588f8fe9c338fae49fa4e16eeb10ffb3d012f23a54578a9e/kiwisolver-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384", size = 73569, upload-time = "2026-03-09T13:13:45.792Z" },
+ { url = "https://files.pythonhosted.org/packages/28/26/192b26196e2316e2bd29deef67e37cdf9870d9af8e085e521afff0fed526/kiwisolver-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7", size = 64997, upload-time = "2026-03-09T13:13:46.878Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/69/024d6711d5ba575aa65d5538042e99964104e97fa153a9f10bc369182bc2/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09", size = 123166, upload-time = "2026-03-09T13:13:48.032Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/48/adbb40df306f587054a348831220812b9b1d787aff714cfbc8556e38fccd/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3", size = 66395, upload-time = "2026-03-09T13:13:49.365Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd", size = 64065, upload-time = "2026-03-09T13:13:50.562Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3", size = 1477903, upload-time = "2026-03-09T13:13:52.084Z" },
+ { url = "https://files.pythonhosted.org/packages/18/d8/55638d89ffd27799d5cc3d8aa28e12f4ce7a64d67b285114dbedc8ea4136/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96", size = 1278751, upload-time = "2026-03-09T13:13:54.673Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/97/b4c8d0d18421ecceba20ad8701358453b88e32414e6f6950b5a4bad54e65/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099", size = 1296793, upload-time = "2026-03-09T13:13:56.287Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/10/f862f94b6389d8957448ec9df59450b81bec4abb318805375c401a1e6892/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8", size = 1346041, upload-time = "2026-03-09T13:13:58.269Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/6a/f1650af35821eaf09de398ec0bc2aefc8f211f0cda50204c9f1673741ba9/kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87", size = 987292, upload-time = "2026-03-09T13:13:59.871Z" },
+ { url = "https://files.pythonhosted.org/packages/de/19/d7fb82984b9238115fe629c915007be608ebd23dc8629703d917dbfaffd4/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23", size = 2227865, upload-time = "2026-03-09T13:14:01.401Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/b9/46b7f386589fd222dac9e9de9c956ce5bcefe2ee73b4e79891381dda8654/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859", size = 2324369, upload-time = "2026-03-09T13:14:02.972Z" },
+ { url = "https://files.pythonhosted.org/packages/92/8b/95e237cf3d9c642960153c769ddcbe278f182c8affb20cecc1cc983e7cc5/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902", size = 1977989, upload-time = "2026-03-09T13:14:04.503Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/95/980c9df53501892784997820136c01f62bc1865e31b82b9560f980c0e649/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167", size = 2491645, upload-time = "2026-03-09T13:14:06.106Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/32/900647fd0840abebe1561792c6b31e6a7c0e278fc3973d30572a965ca14c/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0", size = 2295237, upload-time = "2026-03-09T13:14:08.891Z" },
+ { url = "https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276", size = 73573, upload-time = "2026-03-09T13:14:12.327Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/d2/64be2e429eb4fca7f7e1c52a91b12663aeaf25de3895e5cca0f47ef2a8d0/kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c", size = 64998, upload-time = "2026-03-09T13:14:13.469Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/69/ce68dd0c85755ae2de490bf015b62f2cea5f6b14ff00a463f9d0774449ff/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1", size = 125700, upload-time = "2026-03-09T13:14:14.636Z" },
+ { url = "https://files.pythonhosted.org/packages/74/aa/937aac021cf9d4349990d47eb319309a51355ed1dbdc9c077cdc9224cb11/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e", size = 67537, upload-time = "2026-03-09T13:14:15.808Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/20/3a87fbece2c40ad0f6f0aefa93542559159c5f99831d596050e8afae7a9f/kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7", size = 65514, upload-time = "2026-03-09T13:14:18.035Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/7f/f943879cda9007c45e1f7dba216d705c3a18d6b35830e488b6c6a4e7cdf0/kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c", size = 1584848, upload-time = "2026-03-09T13:14:19.745Z" },
+ { url = "https://files.pythonhosted.org/packages/37/f8/4d4f85cc1870c127c88d950913370dd76138482161cd07eabbc450deff01/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368", size = 1391542, upload-time = "2026-03-09T13:14:21.54Z" },
+ { url = "https://files.pythonhosted.org/packages/04/0b/65dd2916c84d252b244bd405303220f729e7c17c9d7d33dca6feeff9ffc4/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489", size = 1404447, upload-time = "2026-03-09T13:14:23.205Z" },
+ { url = "https://files.pythonhosted.org/packages/39/5c/2606a373247babce9b1d056c03a04b65f3cf5290a8eac5d7bdead0a17e21/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1", size = 1455918, upload-time = "2026-03-09T13:14:24.74Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/d1/c6078b5756670658e9192a2ef11e939c92918833d2745f85cd14a6004bdf/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3", size = 1072856, upload-time = "2026-03-09T13:14:26.597Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/c8/7def6ddf16eb2b3741d8b172bdaa9af882b03c78e9b0772975408801fa63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18", size = 2333580, upload-time = "2026-03-09T13:14:28.237Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/87/2ac1fce0eb1e616fcd3c35caa23e665e9b1948bb984f4764790924594128/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021", size = 2423018, upload-time = "2026-03-09T13:14:30.018Z" },
+ { url = "https://files.pythonhosted.org/packages/67/13/c6700ccc6cc218716bfcda4935e4b2997039869b4ad8a94f364c5a3b8e63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310", size = 2062804, upload-time = "2026-03-09T13:14:32.888Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/bd/877056304626943ff0f1f44c08f584300c199b887cb3176cd7e34f1515f1/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3", size = 2597482, upload-time = "2026-03-09T13:14:34.971Z" },
+ { url = "https://files.pythonhosted.org/packages/75/19/c60626c47bf0f8ac5dcf72c6c98e266d714f2fbbfd50cf6dab5ede3aaa50/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2", size = 2394328, upload-time = "2026-03-09T13:14:36.816Z" },
+ { url = "https://files.pythonhosted.org/packages/47/84/6a6d5e5bb8273756c27b7d810d47f7ef2f1f9b9fd23c9ee9a3f8c75c9cef/kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53", size = 68410, upload-time = "2026-03-09T13:14:38.695Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/d7/060f45052f2a01ad5762c8fdecd6d7a752b43400dc29ff75cd47225a40fd/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615", size = 123231, upload-time = "2026-03-09T13:14:41.323Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/a7/78da680eadd06ff35edef6ef68a1ad273bad3e2a0936c9a885103230aece/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02", size = 66489, upload-time = "2026-03-09T13:14:42.534Z" },
+ { url = "https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e", size = 64063, upload-time = "2026-03-09T13:14:44.759Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac", size = 1475913, upload-time = "2026-03-09T13:14:46.247Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05", size = 1282782, upload-time = "2026-03-09T13:14:48.458Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/9f/1de7aad00697325f05238a5f2eafbd487fb637cc27a558b5367a5f37fb7f/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd", size = 1300815, upload-time = "2026-03-09T13:14:50.721Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/c2/297f25141d2e468e0ce7f7a7b92e0cf8918143a0cbd3422c1ad627e85a06/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a", size = 1347925, upload-time = "2026-03-09T13:14:52.304Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/d3/f4c73a02eb41520c47610207b21afa8cdd18fdbf64ffd94674ae21c4812d/kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554", size = 991322, upload-time = "2026-03-09T13:14:54.637Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/46/d3f2efef7732fcda98d22bf4ad5d3d71d545167a852ca710a494f4c15343/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581", size = 2232857, upload-time = "2026-03-09T13:14:56.471Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/ec/2d9756bf2b6d26ae4349b8d3662fb3993f16d80c1f971c179ce862b9dbae/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303", size = 2329376, upload-time = "2026-03-09T13:14:58.072Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/9f/876a0a0f2260f1bde92e002b3019a5fabc35e0939c7d945e0fa66185eb20/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9", size = 1982549, upload-time = "2026-03-09T13:14:59.668Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/4f/ba3624dfac23a64d54ac4179832860cb537c1b0af06024936e82ca4154a0/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79", size = 2494680, upload-time = "2026-03-09T13:15:01.364Z" },
+ { url = "https://files.pythonhosted.org/packages/39/b7/97716b190ab98911b20d10bf92eca469121ec483b8ce0edd314f51bc85af/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796", size = 2297905, upload-time = "2026-03-09T13:15:03.925Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e", size = 75086, upload-time = "2026-03-09T13:15:07.775Z" },
+ { url = "https://files.pythonhosted.org/packages/70/15/9b90f7df0e31a003c71649cf66ef61c3c1b862f48c81007fa2383c8bd8d7/kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df", size = 66577, upload-time = "2026-03-09T13:15:09.139Z" },
+ { url = "https://files.pythonhosted.org/packages/17/01/7dc8c5443ff42b38e72731643ed7cf1ed9bf01691ae5cdca98501999ed83/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e", size = 125794, upload-time = "2026-03-09T13:15:10.525Z" },
+ { url = "https://files.pythonhosted.org/packages/46/8a/b4ebe46ebaac6a303417fab10c2e165c557ddaff558f9699d302b256bc53/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4", size = 67646, upload-time = "2026-03-09T13:15:12.016Z" },
+ { url = "https://files.pythonhosted.org/packages/60/35/10a844afc5f19d6f567359bf4789e26661755a2f36200d5d1ed8ad0126e5/kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028", size = 65511, upload-time = "2026-03-09T13:15:13.311Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/8a/685b297052dd041dcebce8e8787b58923b6e78acc6115a0dc9189011c44b/kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657", size = 1584858, upload-time = "2026-03-09T13:15:15.103Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/80/04865e3d4638ac5bddec28908916df4a3075b8c6cc101786a96803188b96/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920", size = 1392539, upload-time = "2026-03-09T13:15:16.661Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/01/77a19cacc0893fa13fafa46d1bba06fb4dc2360b3292baf4b56d8e067b24/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9", size = 1405310, upload-time = "2026-03-09T13:15:18.229Z" },
+ { url = "https://files.pythonhosted.org/packages/53/39/bcaf5d0cca50e604cfa9b4e3ae1d64b50ca1ae5b754122396084599ef903/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d", size = 1456244, upload-time = "2026-03-09T13:15:20.444Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/7a/72c187abc6975f6978c3e39b7cf67aeb8b3c0a8f9790aa7fd412855e9e1f/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65", size = 1073154, upload-time = "2026-03-09T13:15:22.039Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/ca/cf5b25783ebbd59143b4371ed0c8428a278abe68d6d0104b01865b1bbd0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa", size = 2334377, upload-time = "2026-03-09T13:15:23.741Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/e5/b1f492adc516796e88751282276745340e2a72dcd0d36cf7173e0daf3210/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0", size = 2425288, upload-time = "2026-03-09T13:15:25.789Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/e5/9b21fbe91a61b8f409d74a26498706e97a48008bfcd1864373d32a6ba31c/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", size = 2063158, upload-time = "2026-03-09T13:15:27.63Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/02/83f47986138310f95ea95531f851b2a62227c11cbc3e690ae1374fe49f0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f", size = 2597260, upload-time = "2026-03-09T13:15:29.421Z" },
+ { url = "https://files.pythonhosted.org/packages/07/18/43a5f24608d8c313dd189cf838c8e68d75b115567c6279de7796197cfb6a/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646", size = 2394403, upload-time = "2026-03-09T13:15:31.517Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/b5/98222136d839b8afabcaa943b09bd05888c2d36355b7e448550211d1fca4/kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681", size = 79687, upload-time = "2026-03-09T13:15:33.204Z" },
+ { url = "https://files.pythonhosted.org/packages/99/a2/ca7dc962848040befed12732dff6acae7fb3c4f6fc4272b3f6c9a30b8713/kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57", size = 70032, upload-time = "2026-03-09T13:15:34.411Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/fa/2910df836372d8761bb6eff7d8bdcb1613b5c2e03f260efe7abe34d388a7/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797", size = 130262, upload-time = "2026-03-09T13:15:35.629Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/41/c5f71f9f00aabcc71fee8b7475e3f64747282580c2fe748961ba29b18385/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203", size = 138036, upload-time = "2026-03-09T13:15:36.894Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/06/7399a607f434119c6e1fdc8ec89a8d51ccccadf3341dee4ead6bd14caaf5/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7", size = 194295, upload-time = "2026-03-09T13:15:38.22Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/91/53255615acd2a1eaca307ede3c90eb550bae9c94581f8c00081b6b1c8f44/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57", size = 75987, upload-time = "2026-03-09T13:15:39.65Z" },
+ { url = "https://files.pythonhosted.org/packages/17/6f/6fd4f690a40c2582fa34b97d2678f718acf3706b91d270c65ecb455d0a06/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:295d9ffe712caa9f8a3081de8d32fc60191b4b51c76f02f951fd8407253528f4", size = 59606, upload-time = "2026-03-09T13:15:40.81Z" },
+ { url = "https://files.pythonhosted.org/packages/82/a0/2355d5e3b338f13ce63f361abb181e3b6ea5fffdb73f739b3e80efa76159/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:51e8c4084897de9f05898c2c2a39af6318044ae969d46ff7a34ed3f96274adca", size = 57537, upload-time = "2026-03-09T13:15:42.071Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/b9/1d50e610ecadebe205b71d6728fd224ce0e0ca6aba7b9cbe1da049203ac5/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b83af57bdddef03c01a9138034c6ff03181a3028d9a1003b301eb1a55e161a3f", size = 79888, upload-time = "2026-03-09T13:15:43.317Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/ee/b85ffcd75afed0357d74f0e6fc02a4507da441165de1ca4760b9f496390d/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf4679a3d71012a7c2bf360e5cd878fbd5e4fcac0896b56393dec239d81529ed", size = 77584, upload-time = "2026-03-09T13:15:44.605Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/dd/644d0dde6010a8583b4cd66dd41c5f83f5325464d15c4f490b3340ab73b4/kiwisolver-1.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:41024ed50e44ab1a60d3fe0a9d15a4ccc9f5f2b1d814ff283c8d01134d5b81bc", size = 73390, upload-time = "2026-03-09T13:15:45.832Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/eb/5fcbbbf9a0e2c3a35effb88831a483345326bbc3a030a3b5b69aee647f84/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ec4c85dc4b687c7f7f15f553ff26a98bfe8c58f5f7f0ac8905f0ba4c7be60232", size = 59532, upload-time = "2026-03-09T13:15:47.047Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/9b/e17104555bb4db148fd52327feea1e96be4b88e8e008b029002c281a21ab/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:12e91c215a96e39f57989c8912ae761286ac5a9584d04030ceb3368a357f017a", size = 57420, upload-time = "2026-03-09T13:15:48.199Z" },
+ { url = "https://files.pythonhosted.org/packages/48/44/2b5b95b7aa39fb2d8d9d956e0f3d5d45aef2ae1d942d4c3ffac2f9cfed1a/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be4a51a55833dc29ab5d7503e7bcb3b3af3402d266018137127450005cdfe737", size = 79892, upload-time = "2026-03-09T13:15:49.694Z" },
+ { url = "https://files.pythonhosted.org/packages/52/7d/7157f9bba6b455cfb4632ed411e199fc8b8977642c2b12082e1bd9e6d173/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daae526907e262de627d8f70058a0f64acc9e2641c164c99c8f594b34a799a16", size = 77603, upload-time = "2026-03-09T13:15:50.945Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/dd/8050c947d435c8d4bc94e3252f4d8bb8a76cfb424f043a8680be637a57f1/kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1", size = 73558, upload-time = "2026-03-09T13:15:52.112Z" },
+]
+
[[package]]
name = "libclang"
version = "18.1.1"
@@ -1499,6 +1953,77 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl", hash = "sha256:b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3", size = 4398, upload-time = "2022-04-20T22:04:42.23Z" },
]
+[[package]]
+name = "logbook"
+version = "1.9.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f2/cf/e39c249003caaa7f84e2b00c11c7423892d525f8136ff349f9914914a744/logbook-1.9.2.tar.gz", hash = "sha256:0538cabfd8e8a02b8185fb7a2be20b3965d225fbd7f4a5726b007d8e26b39ee0", size = 481718, upload-time = "2025-11-27T21:12:02.539Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3e/ea/ebaec6de5172e8cc5e2fe42b31766ff351e1675b0ba6e671261aad186d7f/logbook-1.9.2-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:6206c062144c9cdf4ca7df74df22de21d38a93a2197ff45b74c5619962b504de", size = 609607, upload-time = "2025-11-27T21:10:16.511Z" },
+ { url = "https://files.pythonhosted.org/packages/17/5f/13dd76cb4dcc4248ea13779fd166d7e72f9e1d55afae677a094e39219bd1/logbook-1.9.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:81ebf48a67a87392e80e62ea86ee1112bbee7c1823b9e23c1a3b545f5af9cdf5", size = 341376, upload-time = "2025-11-27T21:10:17.914Z" },
+ { url = "https://files.pythonhosted.org/packages/76/c3/5ee40d25e91b631d079071cea0275cd30e19d68de440780957e06899403f/logbook-1.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2d7d83da6a955dfd7f1bdb03c9453dc8a042474de2246902a74ec673207ea6b8", size = 332480, upload-time = "2025-11-27T21:10:19.211Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/2e/4df389e943097ac3b791ee40e5b34b90365c75ae5869a74238bd29ae3421/logbook-1.9.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:8174a413f553dd6b8d5a4fcceab3fead7fe4632961cfdf253f82e3c5f807ed19", size = 363269, upload-time = "2025-11-27T21:10:20.595Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/1d/3f4467fe2439e240ce0dba7bb7e8e2b06d048ac0208457720ca5ecd70ac9/logbook-1.9.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:bad04a6d8af4064d003607fff57b49fb16f50d381a07016bb3d58922bd6d7d9d", size = 369752, upload-time = "2025-11-27T21:10:21.91Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/e1/db9858e14a219b12dbac8b395bfbd32364be6e00bc84ec9ef134c32d31ec/logbook-1.9.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab1ef0ba6db99e866d81e0e4d0ec1776d995359400057ab7345693a0c5dd4a02", size = 427817, upload-time = "2025-11-27T21:10:23.412Z" },
+ { url = "https://files.pythonhosted.org/packages/33/3f/d85c102275be4d12be1c574f36b62ca2f78652dcf0ce18aceb7d8980856a/logbook-1.9.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e12e05944029cd453b46d4be085a7127f2465b95210a4b89fa734c8881624539", size = 443621, upload-time = "2025-11-27T21:10:24.87Z" },
+ { url = "https://files.pythonhosted.org/packages/68/8b/1d8dc3a7ca77145180a02f8f901c9671c410df50d89893ce684b17312711/logbook-1.9.2-cp310-cp310-win32.whl", hash = "sha256:2247450164ed2d74e78edb0003235e560c018cc2a5b8fb97951bebdd6c93a5a6", size = 215739, upload-time = "2025-11-27T21:10:26.239Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/f9/384e310b773b0620580facfe1178f8c46009ef6d2f6c5846353a899d4992/logbook-1.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:493ad0f04175f31c739e55333de4d0da32de19757e329309283d19cb38d92a26", size = 221577, upload-time = "2025-11-27T21:10:27.604Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/1f/e4ee906ff18fb60d10b308d5b05dc8d0ce40e7edcd5b8fc9cea80994be9a/logbook-1.9.2-cp310-cp310-win_arm64.whl", hash = "sha256:14eac88f6ac95f2b5cd6c624e99c24a8ef27c49a2c794c4ed51f38c89291bd34", size = 217492, upload-time = "2025-11-27T21:10:29.291Z" },
+ { url = "https://files.pythonhosted.org/packages/84/0c/ef05117fb10ee4ed233e2918662e248fcdc5a9b253fd1b7ce3cc2ea5ba9f/logbook-1.9.2-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:abaa8e1c99f01476077339a34ed6d39f624749bbef64a22bcc646906c23567e9", size = 608780, upload-time = "2025-11-27T21:10:30.98Z" },
+ { url = "https://files.pythonhosted.org/packages/69/9f/0b9e6722dfecf62d657c19db0a5f4aa284b78c825badd6f76e5a651eca1c/logbook-1.9.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:e244cec50ca4503d7d19e5dc8b93d0da12124b45205be36e1c327da0471eab55", size = 341044, upload-time = "2025-11-27T21:10:32.724Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/dc/796b97840ccc5440cdf8dbe78b2afebf0b9230c6d4519f5bca70a1c14cb9/logbook-1.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:57cd0a668e1cd171f086e963dccc79fd55c160010603304782ed5af15e273032", size = 332264, upload-time = "2025-11-27T21:10:34.498Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/ec/80de5880169cd458299fceb19e1f9f14d5f8a75b9ac3f09c56789bb403c3/logbook-1.9.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:068da439a77c95863ee521b8e23281a5734ec15b683b371fbe3ae70250393ac5", size = 363382, upload-time = "2025-11-27T21:10:36.298Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/0e/da2dea269cd32a0866744f2d64ad499d44c722ae161d5a2048f534db4ee1/logbook-1.9.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:c1c913500067560697db9b6fe17b158e3f20f06a1b8687920fac63b209ceae1b", size = 369993, upload-time = "2025-11-27T21:10:37.604Z" },
+ { url = "https://files.pythonhosted.org/packages/44/67/b13dc09c44bb6226b427593a33ceb20d54d0b680defc135ddd9d42dcbd55/logbook-1.9.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7314bffd4c1354e5de6502774b609120aa5e881a820d8ac77431dc96ec18d1d5", size = 428097, upload-time = "2025-11-27T21:10:38.927Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/73/a501496f806c31c0d62bcc660626b12682b8ad871ca66b5330fc96d9898d/logbook-1.9.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3df4412d0d464f04843ab94aabbe503afb9ff20ec982091370eb5245daac842e", size = 443986, upload-time = "2025-11-27T21:10:40.307Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/22/6fc98fe475cf7c428be6cdb7400054608178c13b100666097590d37bf6be/logbook-1.9.2-cp311-cp311-win32.whl", hash = "sha256:4944b9052bfb450ccbc1bbada381fe9ba161aa024f02e7f72b6ad157fd0f1aff", size = 215222, upload-time = "2025-11-27T21:10:41.738Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/df/d5197125a12b55f50c640baf9744fdad4c8c05a77f9a20801b68224f0fdc/logbook-1.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:8dc11dd0ab88453de405620684b530a79110289136b1beb732c8ad3c6ad106f9", size = 221838, upload-time = "2025-11-27T21:10:43.359Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/4b/0d3f427ff7dcaa69d9cf159a76a1ccea4ebb09cfa602da2fe09850c89cd1/logbook-1.9.2-cp311-cp311-win_arm64.whl", hash = "sha256:d3e5288a963180a336012d12f2cb5143dd565c4b879aa00a2d0fce0350089679", size = 217653, upload-time = "2025-11-27T21:10:44.706Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/5c/dd429f46497e3f6c15498b8bd57d5b744247b67675e7df649c8b28065e45/logbook-1.9.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1b60d31dad31da9b108a1c656be7560b930d90b1fff1efbd66a926f0562ec535", size = 605842, upload-time = "2025-11-27T21:10:46.532Z" },
+ { url = "https://files.pythonhosted.org/packages/91/b5/212d37a83690277ac2f8269d4f2b4148909939b656d3ad0f6d3275e8f419/logbook-1.9.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:057743e915c929c9e910b4fa1424531e255de8647c4f3288a086b7c2bb53a3e0", size = 340453, upload-time = "2025-11-27T21:10:48.164Z" },
+ { url = "https://files.pythonhosted.org/packages/17/b5/d36bccf730d7f7d127f0baf2807895f85d5a3f911b99bcbc8e18b3e439f9/logbook-1.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:684d9ade464eace999a3c32e912f3e92dcd6958dd74f2a908b2fffcf376d8433", size = 330194, upload-time = "2025-11-27T21:10:49.526Z" },
+ { url = "https://files.pythonhosted.org/packages/01/35/eaea42346f62c785af2073fbbef115d53aa7422917e2c73ede14b2ab0775/logbook-1.9.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:0e085878f9de9c62d9521e1b920253cd725b94ce291252e83843dd95fc16072f", size = 362301, upload-time = "2025-11-27T21:10:50.879Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/17/8a36848cf9eedf32975cc04000f8b7d8806461ef4702a316cb381e447a2f/logbook-1.9.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:42e258cea92ff422426a76937926675350bd85d9d23899ae21fa70112f041f97", size = 367958, upload-time = "2025-11-27T21:10:52.172Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/e3/8a5a7136421e7090c94ccb83f625c9f946bba17bdf58f02728f4d1a1f898/logbook-1.9.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6fd699fbf1aac8ccc06a131cbe0a652ae150e1bd902f1cacaff9b23ee18ab451", size = 426613, upload-time = "2025-11-27T21:10:53.939Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/0b/ad5c677d9eb8ed3b139c7d7033ccd0d48a5af2e4270a0ed452bd45337d50/logbook-1.9.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35a95af2e3485e5ddb2a6e26dc61f65ffc2895df54af6426e6d07e28efab0cb5", size = 442118, upload-time = "2025-11-27T21:10:55.475Z" },
+ { url = "https://files.pythonhosted.org/packages/36/07/ef0498e8805eb9fa6f5a347ce7569302875d96d071b34f0cf52b1671d111/logbook-1.9.2-cp312-cp312-win32.whl", hash = "sha256:cccc1e347e80faab592e751f52297d8740d638202b191c5c1669373f78b31747", size = 213424, upload-time = "2025-11-27T21:10:57.518Z" },
+ { url = "https://files.pythonhosted.org/packages/86/cf/313171c253e7d69deec15231fbf69132a3b704e7394d6af4fbfcd3568bc2/logbook-1.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:75fb010c56dbe3052924ec14f9a0a64b922839f71592fb7d2907496e253d153a", size = 220437, upload-time = "2025-11-27T21:10:59.576Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/b0/2685d40edaa3a4d036ebcf5b95206b2772408edd9768e8607bdfec38f3ba/logbook-1.9.2-cp312-cp312-win_arm64.whl", hash = "sha256:3851490b920199573e6b57675da85b7cdfdc785a24d5236917b7c50f3e600f26", size = 215989, upload-time = "2025-11-27T21:11:00.814Z" },
+ { url = "https://files.pythonhosted.org/packages/36/fc/3fb019204164b669c87cb62909d7d18b6019cf3a8c3b9768af7f4b605f1e/logbook-1.9.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ff48ba4c0b265f748b23b5ff84f600733d5a82f765db48e900105eecf7972454", size = 605326, upload-time = "2025-11-27T21:11:02.154Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/3b/e3987116a71e74ca69d890825fb935d25f9e9155a418135cfd5794eb3e39/logbook-1.9.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3b6ebbbb6eeb4ec9e181710485a3057349481d7a20d36080dae19056597cb862", size = 340225, upload-time = "2025-11-27T21:11:03.975Z" },
+ { url = "https://files.pythonhosted.org/packages/21/88/d8d830865e799a02223fa2bd701b8d089d0d4f2c9d0b2c1edcdbb151440c/logbook-1.9.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5606f0449605806f559fddad017c67c5f72bacca04f22648fcd2fc8623283b9c", size = 329887, upload-time = "2025-11-27T21:11:05.634Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/2e/eb12e9228bdef4981d0eb6c14f5807491442b08337ba53b56118aa2c9606/logbook-1.9.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:90e73b301417b2d43b2821aec12203aee5e2cb1938be8ed04fb485012f009e42", size = 361574, upload-time = "2025-11-27T21:11:07.117Z" },
+ { url = "https://files.pythonhosted.org/packages/02/3e/3c38f5407207e994c679e84fc11cb04bc38774d0aa53b8427d4d23b91bda/logbook-1.9.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:21d7993f2679e419199cfcec5673df77f977238ec16b7be3ef1514a4e44c8633", size = 368001, upload-time = "2025-11-27T21:11:08.423Z" },
+ { url = "https://files.pythonhosted.org/packages/20/b0/eea01bb474a627b68cc5407ee089aa9a326c124622a92ae8f95831569311/logbook-1.9.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f0453818719e0d5bda294aa0cf7c717a103c86aff5b4fae3b3d92e318525b191", size = 425936, upload-time = "2025-11-27T21:11:09.751Z" },
+ { url = "https://files.pythonhosted.org/packages/88/60/f179733480102ce85fea95607f001be062fb6aafef04c94703e0e5149558/logbook-1.9.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c6985a5af4379c07fed09f4ed51581d39ab0f3e9d1196077a8c49f99c883599f", size = 442161, upload-time = "2025-11-27T21:11:11.145Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/87/422ff192a7a1c992f7888ab508fc996957f297ddc1482c914f2f91e51041/logbook-1.9.2-cp313-cp313-win32.whl", hash = "sha256:7fbebf2a612f5c15309bbba0bef4f7d33af461c05a4d9d19cb376624c7030155", size = 213485, upload-time = "2025-11-27T21:11:12.476Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/9c/2fa7df1b676d9179d5f0040de2272d8b9cbdd820c20da9abfd4abcde3cf0/logbook-1.9.2-cp313-cp313-win_amd64.whl", hash = "sha256:5ce7898b35b836da0e22120b965160ac2e797a19f4694ccb9e23e8ad45b1e99a", size = 220311, upload-time = "2025-11-27T21:11:14.179Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/77/7253384fbb7229622b5439fcf79fd3147fe4bdfa144fea4e1f77b532d935/logbook-1.9.2-cp313-cp313-win_arm64.whl", hash = "sha256:f6a2d8481babaafacf68732ede6269e52c5a5d2a51fba98808d6dea65abb42f2", size = 215922, upload-time = "2025-11-27T21:11:15.523Z" },
+ { url = "https://files.pythonhosted.org/packages/06/0f/fe3d23730a86b63d23b77c8495b9613f93d9bfa347221553c7f9bee2d3b0/logbook-1.9.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:ed154dbb7ab1434893f7504ac6ad5c15d271007c612d9ebe4b07c22139243a8c", size = 603168, upload-time = "2025-11-27T21:11:16.941Z" },
+ { url = "https://files.pythonhosted.org/packages/81/24/2b8752d157f3c2b6bb22997084c4eade225914b79ba9fd6558c991061366/logbook-1.9.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1334bfa54da7490dfb6fa2305d797fc087e78a5d52fc7e048bf949059710b605", size = 338959, upload-time = "2025-11-27T21:11:18.283Z" },
+ { url = "https://files.pythonhosted.org/packages/94/96/52dc9eca5fb7e4703d2e1f405613a90a1e4341bc8123ee8d957d9b4259d8/logbook-1.9.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e37303eac7aeb36097a88727900fd850feba8a1d1307d01dad52376c14c192ec", size = 328772, upload-time = "2025-11-27T21:11:20.001Z" },
+ { url = "https://files.pythonhosted.org/packages/76/24/3cc8ef94cdf7b04e169c58b49380d48b64a2b1be4a58f2e5186cdada2887/logbook-1.9.2-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:e2331cae6ca65bafae73308e6519f1b2022b26199f7c6c6943bfbec2a674ab26", size = 361215, upload-time = "2025-11-27T21:11:21.311Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/e5/0fa31ab9706b0ceb7d37800557f43d17a5e687104fb73c3fe9fe8db8cb1a/logbook-1.9.2-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:5e3677cdc1aca9816ac7d773e08e8e211d91f629ceebb940fd8e9d43a45824e5", size = 366842, upload-time = "2025-11-27T21:11:22.645Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/43/496448225ebf671901b0c5400bcb1e2682c2c91710b3170edd08b6047e9f/logbook-1.9.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4f97f84cd4a9ca39d5aada3e80aecb60fc2fa1c96fcf46d17462d25b89896dd4", size = 425554, upload-time = "2025-11-27T21:11:24.086Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/a9/6004567e3d4fe13094fe6b85df7d85b22c38716cc798bb370fa752f375a7/logbook-1.9.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5764da8ed1d3453a925900c99712b00f7573544c5ce325b29f511f5dc322f306", size = 441237, upload-time = "2025-11-27T21:11:25.435Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/d0/97aab3a303a667afa814acdd96b45364c94e1b835bd95f993dabc45b8a8f/logbook-1.9.2-cp314-cp314-win32.whl", hash = "sha256:c6ed6333bfd5370102cda007827bebaea389c1132fe097b44260600dc6817438", size = 216350, upload-time = "2025-11-27T21:11:26.733Z" },
+ { url = "https://files.pythonhosted.org/packages/53/41/27381729d389a733f57a149869de62f0d74c82125d799ddb5f2b89eb2094/logbook-1.9.2-cp314-cp314-win_amd64.whl", hash = "sha256:9dbf2ebc09f004eef1bd1466a2bd1008b8088eef799a725e2413753fa613dfa0", size = 224447, upload-time = "2025-11-27T21:11:28.101Z" },
+ { url = "https://files.pythonhosted.org/packages/56/a0/28cf82d3e5e1f3f0e7a7c6c6f36ea0e585f0bb75849443821db2af58e2fd/logbook-1.9.2-cp314-cp314-win_arm64.whl", hash = "sha256:1bf202c6d74e985fb0ded5b16918a28722651ff626271a18e9a250ac988ed2df", size = 220143, upload-time = "2025-11-27T21:11:29.442Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/62/9a29148bb26fb9461abdf81c896dd86f889e70c290f61cd1b8520c18c739/logbook-1.9.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:584694499a7699df50f1c2940d8613dea30917b9a79abf017b948987adfa96a9", size = 600607, upload-time = "2025-11-27T21:11:30.758Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/33/ed175cbc6f2e3e97d3be14b161f0e812bfd0bd1c0b78eb4081f94f64ce51/logbook-1.9.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d6498d3e96e263fb4bdd076c6439b5c480228baf45f42f0a9f94854ce5d9590f", size = 337999, upload-time = "2025-11-27T21:11:32.483Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/0f/b7ee380ed14c0b25149de187b05a360cb9e1871dd9816a35591d2f9aca57/logbook-1.9.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:31ac52761c90c254fec2a217b7f479ed91fc3adfb10543623d289b9514c55c46", size = 326879, upload-time = "2025-11-27T21:11:34.465Z" },
+ { url = "https://files.pythonhosted.org/packages/51/22/a7a2f1fafb8c54eae52245033674bee488194a5a01c37fec628fd26fda22/logbook-1.9.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7ebbc57a32f01a70b19201bf4d5d93f13d785676b67e15cd334f6eee817481d", size = 358294, upload-time = "2025-11-27T21:11:36.314Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/26/4d06ab60ae173966a0f2c75ec3652ea660708d81699b4087b0b06fc8efe8/logbook-1.9.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:5b1f5d1f72400eee806dd3c34c2d2c5f73e3ad9f150c47e06b95fc6952be191d", size = 366145, upload-time = "2025-11-27T21:11:38.853Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/5a/62093c73cce824b46c0e591e1115fdcb8303050bdc563aedce6b246f2fe0/logbook-1.9.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9a1a4b6cd9018ec682dc7e198589c396a4315e9c955ee96c04e877385257fd52", size = 423034, upload-time = "2025-11-27T21:11:40.208Z" },
+ { url = "https://files.pythonhosted.org/packages/37/3e/0fe12a5f19880060af72f81704642e3efd732595e45d581b945aad3b1e3e/logbook-1.9.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eed159eca40d3480a98d279c685ca6bea84f4f099cf9a51f09b4acdc9ce1c0b4", size = 439911, upload-time = "2025-11-27T21:11:42.353Z" },
+ { url = "https://files.pythonhosted.org/packages/70/36/abbb24d01c8a3f5a4bef415bb7bb5412997012437dcdf70861d475614172/logbook-1.9.2-cp314-cp314t-win32.whl", hash = "sha256:2f97ab31a5c54c6428a43764df7c88d672a73cb5a01d84ff4b4e56552f33c97b", size = 214478, upload-time = "2025-11-27T21:11:43.682Z" },
+ { url = "https://files.pythonhosted.org/packages/85/d9/26a57c62a3763ffb43338c6d1dbee13b9282e6185ba1092ed471f82e52a8/logbook-1.9.2-cp314-cp314t-win_amd64.whl", hash = "sha256:735f7178e370e65f14536cbd7b157d1b1bf8d7ca55b91ff437dc18a8dbdaaae2", size = 223130, upload-time = "2025-11-27T21:11:45.329Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/c5/5396f5aea4f39a1299bda2616c9d4a59e54eda2c3d229122de5a61e2db2c/logbook-1.9.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e1d743512d5bf9fd73047b16af5660cd9f3168dac4f5880a160cacacd3f53550", size = 217383, upload-time = "2025-11-27T21:11:46.601Z" },
+]
+
[[package]]
name = "markdown"
version = "3.10.2"
@@ -1610,6 +2135,80 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/be/2f/5108cb3ee4ba6501748c4908b908e55f42a5b66245b4cfe0c99326e1ef6e/marshmallow-3.26.2-py3-none-any.whl", hash = "sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73", size = 50964, upload-time = "2025-12-22T06:53:51.801Z" },
]
+[[package]]
+name = "matplotlib"
+version = "3.10.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "cycler" },
+ { name = "fonttools" },
+ { name = "kiwisolver" },
+ { name = "numpy" },
+ { name = "packaging" },
+ { name = "pillow" },
+ { name = "pyparsing" },
+ { name = "python-dateutil" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/63/1b/4be5be87d43d327a0cf4de1a56e86f7f84c89312452406cf122efe2839e6/matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358", size = 34811233, upload-time = "2026-04-24T00:14:13.539Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/6f/340b04986e67aac6f66c5145ce68bf72c64bed30f92c8913499a6e6b8f99/matplotlib-3.10.9-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77210dce9cb8153dffc967efaae990543392563d5a376d4dd8539bebcb0ed217", size = 8296625, upload-time = "2026-04-24T00:11:43.376Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/2f/127081eb83162053ebb9678ceac64220b93a663e0167432566e9c7c82aab/matplotlib-3.10.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1e7698ac9868428e84d2c967424803b2472ff7167d9d6590d4204ed775343c3b", size = 8188790, upload-time = "2026-04-24T00:11:46.556Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/b7/d8bcec2626c35f96972bff656299fef4578113ea6193c8fdad324710410c/matplotlib-3.10.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1aa972116abb4c9d201bf245620b433726cb6856f3bef6a78f776a00f5c92d37", size = 8769389, upload-time = "2026-04-24T00:11:48.959Z" },
+ { url = "https://files.pythonhosted.org/packages/12/49/b78e214a527ea732033b7f4d37f7afb504d74ba9d134bd47938230dfb8b1/matplotlib-3.10.9-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae2f11957b27ce53497dd4d7b235c4d4f1faf383dfb39d0c5beb833bff883294", size = 9589657, upload-time = "2026-04-24T00:11:51.915Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/15/5246f7b43beae19c74dfee651d58d6cc8112e06f77adb4e88cc04f2e3a23/matplotlib-3.10.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b049278ddce116aaa1c1377ebf58adea909132dfce0281cf7e3a1ea9fc2e2c65", size = 9651983, upload-time = "2026-04-24T00:11:54.766Z" },
+ { url = "https://files.pythonhosted.org/packages/75/77/5acecfe672ba0fa1b8c0454f69ce155d1e6fc5852fa7206bf9afaf767121/matplotlib-3.10.9-cp310-cp310-win_amd64.whl", hash = "sha256:82834c3c292d24d3a8aae77cd2d20019de69d692a34a970e4fdb8d33e2ea3dda", size = 8199701, upload-time = "2026-04-24T00:11:58.389Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/8c/290f021104741fea63769c31494f5324c0cd249bf536a65a4350767b1f22/matplotlib-3.10.9-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:68cfdcede415f7c8f5577b03303dd94526cdb6d11036cecdc205e08733b2d2bb", size = 8306860, upload-time = "2026-04-24T00:12:01.207Z" },
+ { url = "https://files.pythonhosted.org/packages/51/18/325cd32ece1120d1da51cc4e4294c6580190699490183fc2fe8cb6d61ec5/matplotlib-3.10.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfca0129678bd56379db26c52b5d77ed7de314c047492fbdc763aa7501710cfb", size = 8199254, upload-time = "2026-04-24T00:12:04.239Z" },
+ { url = "https://files.pythonhosted.org/packages/79/db/e28c1b83e3680740aa78925f5fb2ae4d16207207419ad75ea9fe604f8676/matplotlib-3.10.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e436d155fa8a3399dc62683f8f5d0e2e50d25d0144a73edd73f82eec8f4abfb", size = 8777092, upload-time = "2026-04-24T00:12:06.793Z" },
+ { url = "https://files.pythonhosted.org/packages/55/fa/3ce7adfe9ba101748f465211660d9c6374c876b671bdb8c2bb6d347e8b94/matplotlib-3.10.9-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56fc0bd271b00025c6edfdc7c2dcd247372c8e1544971d62e1dc7c17367e8bf9", size = 9595691, upload-time = "2026-04-24T00:12:09.706Z" },
+ { url = "https://files.pythonhosted.org/packages/36/c4/6960a76686ed668f2c60f84e9799ba4c0d56abdb36b1577b60c1d061d1ec/matplotlib-3.10.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5a6104ed666402ba5106d7f36e0e0cdca4e8d7fa4d39708ca88019e2835a2eb", size = 9659771, upload-time = "2026-04-24T00:12:12.766Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/0d/271aace3342157c64700c9ff4c59c7b392f3dbab393692e8db6fbe7ab96c/matplotlib-3.10.9-cp311-cp311-win_amd64.whl", hash = "sha256:d730e984eddf56974c3e72b6129c7ca462ac38dc624338f4b0b23eb23ecba00f", size = 8205112, upload-time = "2026-04-24T00:12:15.773Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/ee/cb57ad4754f3e7b9174ce6ce66d9205fb827067e48a9f58ac09d7e7d6b77/matplotlib-3.10.9-cp311-cp311-win_arm64.whl", hash = "sha256:51bf0ddbdc598e060d46c16b5590708f81a1624cefbaaf62f6a81bf9285b8c80", size = 8132310, upload-time = "2026-04-24T00:12:18.645Z" },
+ { url = "https://files.pythonhosted.org/packages/35/c6/5581e26c72233ebb2a2a6fed2d24fb7c66b4700120b813f51b0555acf0b6/matplotlib-3.10.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0c3c28d9fbcc1fe7a03be236d73430cf6409c41fb2383a7ac52fe932b072cb1", size = 8319908, upload-time = "2026-04-24T00:12:21.323Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/18/4880dd762e40cd360c1bf06e890c5a97b997e91cb324602b1a19950ad5ce/matplotlib-3.10.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cb28c2bd769aa3e98322c6ab09854cbcc52ab69d2759d681bba3e327b2b320", size = 8216016, upload-time = "2026-04-24T00:12:23.4Z" },
+ { url = "https://files.pythonhosted.org/packages/32/91/d024616abdba99e83120e07a20658976f6a343646710760c4a51df126029/matplotlib-3.10.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae20801130378b82d647ff5047c07316295b68dc054ca6b3c13519d0ea624285", size = 8789336, upload-time = "2026-04-24T00:12:26.096Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/04/030a2f61ef2158f5e4c259487a92ac877732499fb33d871585d89e03c42d/matplotlib-3.10.9-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c63ebcd8b4b169eb2f5c200552ae6b8be8999a005b6b507ed76fb8d7d674fe2", size = 9604602, upload-time = "2026-04-24T00:12:29.052Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/c2/541e4d09d87bb6b5830fc28b4c887a9a8cf4e1c6cee698a8c05552ae2003/matplotlib-3.10.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d75d11c949914165976c621b2324f9ef162af7ebf4b057ddf95dd1dba7e5edcf", size = 9670966, upload-time = "2026-04-24T00:12:32.131Z" },
+ { url = "https://files.pythonhosted.org/packages/04/a1/4571fc46e7702de8d0c2dc54ad1b2f8e29328dea3ee90831181f7353d93c/matplotlib-3.10.9-cp312-cp312-win_amd64.whl", hash = "sha256:d091f9d758b34aaaaa6331d13574bf01891d903b3dec59bfff458ef7551de5d6", size = 8217462, upload-time = "2026-04-24T00:12:35.226Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/d0/2269edb12aa30c13c8bcc9382892e39943ce1d28aab4ec296e0381798e81/matplotlib-3.10.9-cp312-cp312-win_arm64.whl", hash = "sha256:10cc5ce06d10231c36f40e875f3c7e8050362a4ee8f0ee5d29a6b3277d57bb42", size = 8136688, upload-time = "2026-04-24T00:12:37.442Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/d3/8d4f6afbecb49fc04e060a57c0fce39ea51cc163a6bd87303ccd698e4fa6/matplotlib-3.10.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b580440f1ff81a0e34122051a3dfabb7e4b7f9e380629929bde0eff9af72165f", size = 8320331, upload-time = "2026-04-24T00:12:39.688Z" },
+ { url = "https://files.pythonhosted.org/packages/63/d9/9e14bc7564bf92d5ffa801ae5fac819ce74b925dfb55e3ebde61a3bbad3e/matplotlib-3.10.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b1b745c489cd1a77a0dc1120a05dc87af9798faebc913601feb8c73d89bf2d1e", size = 8216461, upload-time = "2026-04-24T00:12:42.494Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/17/4402d0d14ccf1dfc70932600b68097fbbf9c898a4871d2cbbe79c7801a32/matplotlib-3.10.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8f3bcac1ca5ed000a6f4337d47ba67dfddf37ed6a46c15fd7f014997f7bf865f", size = 8790091, upload-time = "2026-04-24T00:12:44.789Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/0b/322aeec06dd9b91411f92028b37d447342770a24392aa4813e317064dad5/matplotlib-3.10.9-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a8d66a55def891c33147ba3ba9bfcabf0b526a43764c818acbb4525e5ed0838", size = 9605027, upload-time = "2026-04-24T00:12:47.583Z" },
+ { url = "https://files.pythonhosted.org/packages/74/88/5f13482f55e7b00bcfc09838b093c2456e1379978d2a146844aae05350ad/matplotlib-3.10.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d843374407c4017a6403b59c6c81606773d136f3259d5b6da3131bc814542cc2", size = 9671269, upload-time = "2026-04-24T00:12:50.878Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/e0/0840fd2f93da988ec660b8ad1984abe9f25d2aed22a5e394ff1c68c88307/matplotlib-3.10.9-cp313-cp313-win_amd64.whl", hash = "sha256:f4399f64b3e94cd500195490972ae1ee81170df1636fa15364d157d5bdd7b921", size = 8217588, upload-time = "2026-04-24T00:12:53.784Z" },
+ { url = "https://files.pythonhosted.org/packages/47/b9/d706d06dd605c49b9f83a2aed8c13e3e5db70697d7a80b7e3d7915de6b17/matplotlib-3.10.9-cp313-cp313-win_arm64.whl", hash = "sha256:ba7b3b8ef09eab7df0e86e9ae086faa433efbfbdb46afcb3aa16aabf779469a8", size = 8136913, upload-time = "2026-04-24T00:12:56.501Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/45/6e32d96978264c8ca8c4b1010adb955a1a49cfaf314e212bbc8908f04a61/matplotlib-3.10.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:09218df8a93712bd6ea133e83a153c755448cf7868316c531cffcc43f69d1cc9", size = 8368019, upload-time = "2026-04-24T00:12:58.896Z" },
+ { url = "https://files.pythonhosted.org/packages/86/0a/c8e3d3bba245f0f7fc424937f8ff7ef77291a36af3edb97ccd78aa93d84f/matplotlib-3.10.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:82368699727bfb7b0182e1aa13082e3c08e092fa1a25d3e1fd92405bff96f6d4", size = 8264645, upload-time = "2026-04-24T00:13:01.406Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/aa/5bf5a14fe4fed73a4209a155606f8096ff797aad89c6c35179026571133e/matplotlib-3.10.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3225f4e1edcb8c86c884ddf79ebe20ecd0a67d30188f279897554ccd8fded4dc", size = 8802194, upload-time = "2026-04-24T00:13:03.702Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/5e/b4be852d6bba6fd15893fadf91ff26ae49cb91aac789e95dde9d342e664f/matplotlib-3.10.9-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de2445a0c6690d21b7eb6ce071cebad6d40a2e9bdf10d039074a96ba19797b99", size = 9622684, upload-time = "2026-04-24T00:13:06.647Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/3d/ed428c971139112ef730f62770654d609467346d09d4b62617e1afd68a5a/matplotlib-3.10.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b2b9516251cb89ff618d757daec0e2ed1bf21248013844a853d87ef85ab3081d", size = 9680790, upload-time = "2026-04-24T00:13:10.009Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/09/052e884aaf2b985c63cb79f715f1d5b6a3eaa7de78f6a52b9dbc077d5b53/matplotlib-3.10.9-cp313-cp313t-win_amd64.whl", hash = "sha256:e9fae004b941b23ff2edcf1567a857ed77bafc8086ffa258190462328434faf8", size = 8287571, upload-time = "2026-04-24T00:13:13.087Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/38/ae27288e788c35a4250491422f3db7750366fc8c97d6f36fbdecfc1f5518/matplotlib-3.10.9-cp313-cp313t-win_arm64.whl", hash = "sha256:6b63d9c7c769b88ab81e10dc86e4e0607cf56817b9f9e6cf24b2a5f1693b8e38", size = 8188292, upload-time = "2026-04-24T00:13:15.546Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/e6/3bd8afd04949f02eabc1c17115ea5255e19cacd4d06fc5abdde4eeb0052c/matplotlib-3.10.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:172db52c9e683f5d12eaf57f0f54834190e12581fe1cc2a19595a8f5acb4e77d", size = 8321276, upload-time = "2026-04-24T00:13:18.318Z" },
+ { url = "https://files.pythonhosted.org/packages/41/86/86231232fff41c9f8e4a1a7d7a597d349a02527109c3af7d618366122139/matplotlib-3.10.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97e35e8d39ccc85859095e01a53847432ba9a53ddf7986f7a54a11b73d0e143f", size = 8218218, upload-time = "2026-04-24T00:13:20.974Z" },
+ { url = "https://files.pythonhosted.org/packages/85/8f/becc9722cafc64f5d2eb0b7c1bf5f585271c618a45dbd8fabeb021f898b6/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aba1615dabe83188e19d4f75a253c6a08423e04c1425e64039f800050a69de6b", size = 9608145, upload-time = "2026-04-24T00:13:23.228Z" },
+ { url = "https://files.pythonhosted.org/packages/32/5d/f7e914f7d9325abff4057cee62c0fa70263683189f774473cbfb534cd13b/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34cf8167e023ad956c15f36302911d5406bd99a9862c1a8499ea6f7c0e015dc2", size = 9885085, upload-time = "2026-04-24T00:13:25.849Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/fd/fa69f2221534e80cc5772ac2b7d222011a2acafc2ec7216d5dd174c864ae/matplotlib-3.10.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59476c6d29d612b8e9bb6ce8c5b631be6ba8f9e3a2421f22a02b192c7dd28716", size = 9672358, upload-time = "2026-04-24T00:13:28.906Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/1a/5a4f747a8b271cbb024946d2dd3c913ab5032ba430626f8c3528ada96b4b/matplotlib-3.10.9-cp314-cp314-win_amd64.whl", hash = "sha256:336b9acc64d309063126edcdaca00db9373af3c476bb94388fe9c5a53ad13e6f", size = 8349970, upload-time = "2026-04-24T00:13:31.904Z" },
+ { url = "https://files.pythonhosted.org/packages/64/dc/95d60ecaefe30680a154b52ea96ab4b0dab547f1fd6aa12f5fb655e89cae/matplotlib-3.10.9-cp314-cp314-win_arm64.whl", hash = "sha256:2dc9477819ffd78ad12a20df1d9d6a6bd4fec6aaa9072681465fddca052f1456", size = 8272785, upload-time = "2026-04-24T00:13:34.511Z" },
+ { url = "https://files.pythonhosted.org/packages/70/a0/005d68bc8b8418300ce6591f18586910a8526806e2ab663933d9f20a41e9/matplotlib-3.10.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:da4e09638420548f31c354032a6250e473c68e5a4e96899b4844cf39ddea23fe", size = 8367999, upload-time = "2026-04-24T00:13:36.962Z" },
+ { url = "https://files.pythonhosted.org/packages/22/05/1236cc9290be70b2498af20ca348add76e3fffe7f67b477db5133a84f3ea/matplotlib-3.10.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:345f6f68ecc8da0ca56fad2ea08fde1a115eda530079eca185d50a7bc3e146c6", size = 8264543, upload-time = "2026-04-24T00:13:39.851Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/c2/071f5a5ff6c5bd63aaaf2f45c811d9bf2ced94bde188d9e1a519e21d0cba/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4edcfbd8565339aa62f1cd4012f7180926fdbe71850f7b0d3c379c175cd6b66c", size = 9622800, upload-time = "2026-04-24T00:13:42.296Z" },
+ { url = "https://files.pythonhosted.org/packages/95/57/da7d1f10a85624b9e7db68e069dd94e58dc41dbf9463c5921632ecbe3661/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be157fe17fc37cb95ac1d7374cf717ce9259616edec911a78d9d26dae8522d4", size = 9888561, upload-time = "2026-04-24T00:13:45.026Z" },
+ { url = "https://files.pythonhosted.org/packages/67/b2/ef8d6bb59b0edb6c16c968b70f548aa13b54348972def5aa6ac85df67145/matplotlib-3.10.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4e42042d54db34fda4e95a7bd3e5789c2a995d2dad3eb8850232ee534092fbbf", size = 9680884, upload-time = "2026-04-24T00:13:48.066Z" },
+ { url = "https://files.pythonhosted.org/packages/61/1c/d21bfeb9931881ebe96bcfcff27c7ae4b160ae0ec291a714c42641a56d75/matplotlib-3.10.9-cp314-cp314t-win_amd64.whl", hash = "sha256:c27df8b3848f32a83d1767566595e43cfaa4460380974da06f4279a7ec143c39", size = 8432333, upload-time = "2026-04-24T00:13:51.008Z" },
+ { url = "https://files.pythonhosted.org/packages/78/23/92493c3e6e1b635ccfff146f7b99e674808787915420373ac399283764c2/matplotlib-3.10.9-cp314-cp314t-win_arm64.whl", hash = "sha256:a49f1eadc84ca85fd72fa4e89e70e61bf86452df6f971af04b12c60761a0772c", size = 8324785, upload-time = "2026-04-24T00:13:53.633Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/2b/0e92ad0ac446633f928a1563db4aa8add407e1924faf0ded5b95b35afb27/matplotlib-3.10.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1872fb212a05b729e649754a72d5da61d03e0554d76e80303b6f83d1d2c0552b", size = 8293058, upload-time = "2026-04-24T00:13:56.339Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/23/74682fd369f5299ceda438fea2a0662e6383b85c9383fb9cdfcf04713e07/matplotlib-3.10.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:985f2238880e2e69093f588f5fe2e46771747febf0649f3cf7f7b7480875317f", size = 8186627, upload-time = "2026-04-24T00:13:58.623Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/e8/368aab88f3c4cd8992800f31abfe0670c3e47540ba20a97e9fdbcde594b3/matplotlib-3.10.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6640f75af2c6148293caa0a2b39dd806a492dd66c8a8b04035813e33d0fd2585", size = 8764117, upload-time = "2026-04-24T00:14:01.684Z" },
+ { url = "https://files.pythonhosted.org/packages/63/e2/9f66ca6a651a52abfe0d4964ce01439ed34f3f1e119de10ff3a07f403043/matplotlib-3.10.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:42fb814efabe95c06c1994d8ab5a8385f43a249e23badd3ba931d4308e5bca20", size = 8304420, upload-time = "2026-04-24T00:14:04.57Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/e8/467c03568218792906aa87b5e7bb379b605e056ed0c74fe00c051786d925/matplotlib-3.10.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f76e640a5268850bfda54b5131b1b1941cc685e42c5fa98ed9f2d64038308cba", size = 8197981, upload-time = "2026-04-24T00:14:07.233Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/87/afead29192170917537934c6aff4b008c805fff7b1ccea0c79120d96beda/matplotlib-3.10.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3fc0364dfbe1d07f6d15c5ebd0c5bf89e126916e5a8667dd4a7a6e84c36653d4", size = 8774002, upload-time = "2026-04-24T00:14:09.816Z" },
+]
+
[[package]]
name = "mdurl"
version = "0.1.2"
@@ -1624,7 +2223,7 @@ name = "ml-dtypes"
version = "0.3.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "numpy" },
+ { name = "numpy", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/39/7d/8d85fcba868758b3a546e6914e727abd8f29ea6918079f816975c9eecd63/ml_dtypes-0.3.2.tar.gz", hash = "sha256:533059bc5f1764fac071ef54598db358c167c51a718f68f5bb55e3dee79d2967", size = 692014, upload-time = "2024-01-03T19:21:23.615Z" }
wheels = [
@@ -1649,7 +2248,8 @@ source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.11' and sys_platform == 'darwin'",
"python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')",
+ "python_full_version < '3.11' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_machine != 'ARM64' and sys_platform == 'win32') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')",
]
dependencies = [
{ name = "attrs", marker = "python_full_version < '3.11'" },
@@ -1666,13 +2266,16 @@ name = "morecantile"
version = "7.0.3"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
- "python_full_version >= '3.14' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
"python_full_version >= '3.14' and sys_platform == 'darwin'",
"python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
- "python_full_version == '3.13.*' and sys_platform == 'win32'",
- "python_full_version == '3.12.*' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version == '3.13.*' and sys_platform == 'emscripten'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten'",
"python_full_version == '3.13.*' and sys_platform == 'darwin'",
@@ -1681,7 +2284,8 @@ resolution-markers = [
"python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
"(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
- "python_full_version == '3.11.*' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version == '3.11.*' and sys_platform == 'emscripten'",
"python_full_version == '3.11.*' and sys_platform == 'darwin'",
"python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
@@ -1898,6 +2502,83 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" },
]
+[[package]]
+name = "netcdf4"
+version = "1.7.3"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version < '3.11' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+]
+dependencies = [
+ { name = "certifi", marker = "platform_machine == 'ARM64' and sys_platform == 'win32'" },
+ { name = "cftime", marker = "platform_machine == 'ARM64' and sys_platform == 'win32'" },
+ { name = "numpy", marker = "platform_machine == 'ARM64' and sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0e/76/7bc801796dee752c1ce9cd6935564a6ee79d5c9d9ef9192f57b156495a35/netcdf4-1.7.3.tar.gz", hash = "sha256:83f122fc3415e92b1d4904fd6a0898468b5404c09432c34beb6b16c533884673", size = 836095, upload-time = "2025-10-13T18:38:00.76Z" }
+
+[[package]]
+name = "netcdf4"
+version = "1.7.4"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and sys_platform == 'emscripten'",
+ "python_full_version >= '3.14' and sys_platform == 'darwin'",
+ "python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
+ "python_full_version == '3.13.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and sys_platform == 'emscripten'",
+ "python_full_version == '3.12.*' and sys_platform == 'emscripten'",
+ "python_full_version == '3.13.*' and sys_platform == 'darwin'",
+ "python_full_version == '3.12.*' and sys_platform == 'darwin'",
+ "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
+ "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
+ "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and sys_platform == 'emscripten'",
+ "python_full_version == '3.11.*' and sys_platform == 'darwin'",
+ "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
+ "python_full_version < '3.11' and sys_platform == 'darwin'",
+ "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_machine != 'ARM64' and sys_platform == 'win32') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')",
+]
+dependencies = [
+ { name = "certifi", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "cftime", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "numpy", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/34/b6/0370bb3af66a12098da06dc5843f3b349b7c83ccbdf7306e7afa6248b533/netcdf4-1.7.4.tar.gz", hash = "sha256:cdbfdc92d6f4d7192ca8506c9b3d4c1d9892969ff28d8e8e1fc97ca08bf12164", size = 838352, upload-time = "2026-01-05T02:27:38.593Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0f/07/dfdd017641e82fadaf4e043d91fa179d34940c7d69175a3034dea877df9c/netcdf4-1.7.4-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:b1c1a7ea3678db76bf33d14f7e202385d634db38c5e70d8cf4895971023eebb9", size = 23499427, upload-time = "2026-01-05T02:26:54.13Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/6c/8cd98d166f30d378488c5235457d6af7df09f9925ab5ad03d6840543f42e/netcdf4-1.7.4-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:d3f9497873454207f9480847d02b1b19a4bc81ad6e9166e1c17d4e2f8f3555d1", size = 22886591, upload-time = "2026-01-05T02:26:57.113Z" },
+ { url = "https://files.pythonhosted.org/packages/08/1c/ab31713a95160ebc6b4ec495cd4f03f38b235188a7e955bf33703c5039ca/netcdf4-1.7.4-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8e18294af803e80f8c0339f791901942e268c334c099bbd5f7ea8325a49801a", size = 10336881, upload-time = "2026-01-05T02:26:59.382Z" },
+ { url = "https://files.pythonhosted.org/packages/26/d7/bb16993af267acda23fe3de4ead2528cbe49043e391f732a1a4a15beec20/netcdf4-1.7.4-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0b06c0b93fd0ecc1ec67a582f3ba98b7db9da1fa843c8f83fd75990e3701771e", size = 10182772, upload-time = "2026-01-05T02:27:01.545Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/a6/e6fca338488a896c5e1f661ba3007e83f46700e1a59552b05013d501bc45/netcdf4-1.7.4-cp310-cp310-win_amd64.whl", hash = "sha256:889ba77f084504aebaba9c6f9a88ac213431fef0e897f887cd35aef351ff7740", size = 21363337, upload-time = "2026-01-05T02:27:04.21Z" },
+ { url = "https://files.pythonhosted.org/packages/38/de/38ed7e1956943d28e8ea74161e97c3a00fb98d6d08943b4fd21bae32c240/netcdf4-1.7.4-cp311-abi3-macosx_13_0_x86_64.whl", hash = "sha256:dec70e809cc65b04ebe95113ee9c85ba46a51c3a37c058d2b2b0cadc4d3052d8", size = 23427499, upload-time = "2026-01-05T02:27:06.568Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/70/2f73c133b71709c412bc81d8b721e28dc6237ba9d7dad861b7bfbb70408a/netcdf4-1.7.4-cp311-abi3-macosx_14_0_arm64.whl", hash = "sha256:75cf59100f0775bc4d6b9d4aca7cbabd12e2b8cf3b9a4fb16d810b92743a315a", size = 22847667, upload-time = "2026-01-05T02:27:09.421Z" },
+ { url = "https://files.pythonhosted.org/packages/77/ce/43a3c0c41a6e2e940d87feea79d29aa88302211ac122604838f8a5a48de6/netcdf4-1.7.4-cp311-abi3-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ddfc7e9d261125c74708119440c85ea288b5fee41db676d2ba1ce9be11f96932", size = 10274769, upload-time = "2026-01-05T21:31:19.243Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/7a/a8d32501bb95ecff342004a674720164f95ad616f269450b3bc13dc88ae3/netcdf4-1.7.4-cp311-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a72c9f58767779ec14cb7451c3b56bdd8fdc027a792fac2062b14e090c5617f3", size = 10123122, upload-time = "2026-01-05T21:31:22.773Z" },
+ { url = "https://files.pythonhosted.org/packages/18/68/e89b4fa9242e59326c849c39ce0f49eb68499603c639405a8449900a4f15/netcdf4-1.7.4-cp311-abi3-win_amd64.whl", hash = "sha256:9476e1f23161ae5159cd1548c50c8a37922e77d76583e247133f256ef7b825fc", size = 21299637, upload-time = "2026-01-05T02:27:11.856Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/fc/edd41a3607241027aa4533e7f18e0cd647e74dde10a63274c65350f59967/netcdf4-1.7.4-cp311-abi3-win_arm64.whl", hash = "sha256:876ad9d58f09c98741c066c726164c45a098a58fb90e5fac9e74de4bb8a793fd", size = 2386377, upload-time = "2026-01-05T02:27:13.808Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/3e/1e83534ba68459bc5ae39df46fa71003984df58aabf31f7dcd6e22ecddb0/netcdf4-1.7.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56688c03444fffe0d0c7512cb45245e650389cd841c955b30e4552fa681c4cd9", size = 10519821, upload-time = "2026-01-05T02:27:15.413Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/8c/a15d6fe97f81d6d5202b17838a9a298b5955b3e9971e20609195112829b5/netcdf4-1.7.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ecf471ba8a6ddb2200121949bedfa0095db228822f38227d5da680694a38358", size = 10371133, upload-time = "2026-01-05T02:27:17.224Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/2b/684b15dd4791f8be295b2f6fa97377bbc07a768478a63b7d3c4951712e36/netcdf4-1.7.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5841de0735e8e4875b367c668e81d334287858d64dd9f3e3e2261e808c84922", size = 10395635, upload-time = "2026-01-05T02:27:19.655Z" },
+ { url = "https://files.pythonhosted.org/packages/37/dc/44d21524cf1b1c64254f92e22395a7a10f70c18f3a13a18ac9db258760f7/netcdf4-1.7.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86fac03a8c5b250d57866e7d98918a64742e4b0de1681c5c86bac5726bab8aee", size = 10237725, upload-time = "2026-01-05T02:27:22.298Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/9d/c3ddf54296ad8f18f02f77f23452bdb0971aece1b87e84bab9d734bf72cc/netcdf4-1.7.4-cp314-cp314t-macosx_13_0_x86_64.whl", hash = "sha256:ad083d260301b5add74b1669c75ab0df03bdf986decfcc092cb45eec2615b5f1", size = 23515258, upload-time = "2026-01-05T02:27:24.837Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/44/bc0346e995d436d03fab682b7fbd2a9adcf0db6a05790b8f24853bf08170/netcdf4-1.7.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:7f22014092cc9da3f056b0368e2e38c42afd5725c87ad4843eb2f467e16dd4f6", size = 22910171, upload-time = "2026-01-05T02:27:27.166Z" },
+ { url = "https://files.pythonhosted.org/packages/30/6b/f9bc3f43c55e2dac72ee9f98d77860789bdd5d50c29adf164a6bdb303078/netcdf4-1.7.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:224a15434c165a5e0225e5831f591edf62533044b1ce62fdfee815195bbd077d", size = 10567579, upload-time = "2026-01-05T02:27:29.382Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/d5/e7685c66b7f011c73cd746127f986358a26c642a4e4a1aa5ab51481b6586/netcdf4-1.7.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31a2318305de6831a18df25ad0df9f03b6d68666af0356d4f6057d66c02ffeb6", size = 10255032, upload-time = "2026-01-05T02:27:31.744Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/14/7506738bb6c8bc373b01e5af8f3b727f83f4f496c6b108490ea2609dc2cf/netcdf4-1.7.4-cp314-cp314t-win_amd64.whl", hash = "sha256:6c4a0aa9446c3a616ef3be015b629dc6173643f8b09546de26a4e40e272cd1ed", size = 22289653, upload-time = "2026-01-05T02:27:34.294Z" },
+ { url = "https://files.pythonhosted.org/packages/af/2e/39d5e9179c543f2e6e149a65908f83afd9b6d64379a90789b323111761db/netcdf4-1.7.4-cp314-cp314t-win_arm64.whl", hash = "sha256:034220887d48da032cb2db5958f69759dbb04eb33e279ec6390571d4aea734fe", size = 2531682, upload-time = "2026-01-05T02:27:37.062Z" },
+]
+
[[package]]
name = "numcodecs"
version = "0.13.1"
@@ -1905,7 +2586,8 @@ source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.11' and sys_platform == 'darwin'",
"python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')",
+ "python_full_version < '3.11' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_machine != 'ARM64' and sys_platform == 'win32') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')",
]
dependencies = [
{ name = "numpy", marker = "python_full_version < '3.11'" },
@@ -1935,13 +2617,16 @@ name = "numcodecs"
version = "0.16.5"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
- "python_full_version >= '3.14' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
"python_full_version >= '3.14' and sys_platform == 'darwin'",
"python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
- "python_full_version == '3.13.*' and sys_platform == 'win32'",
- "python_full_version == '3.12.*' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version == '3.13.*' and sys_platform == 'emscripten'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten'",
"python_full_version == '3.13.*' and sys_platform == 'darwin'",
@@ -1950,7 +2635,8 @@ resolution-markers = [
"python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
"(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
- "python_full_version == '3.11.*' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version == '3.11.*' and sys_platform == 'emscripten'",
"python_full_version == '3.11.*' and sys_platform == 'darwin'",
"python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
@@ -2131,7 +2817,7 @@ name = "optree"
version = "0.19.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "typing-extensions" },
+ { name = "typing-extensions", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/44/63/92328a17ab7836562fe0129e605f685a88db35ce98427c34ff48ee4ec157/optree-0.19.1.tar.gz", hash = "sha256:4497d1c9197b8c6842e511368163d318ce536521ebdcff8bebb7551dcdfac532", size = 177531, upload-time = "2026-05-06T02:32:39.704Z" }
wheels = [
@@ -2304,6 +2990,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/67/ad/f9f1d867d0d308b8506c961731410c248b5002dad5dab7419cf67080ff00/osm2geojson-0.3.2-py3-none-any.whl", hash = "sha256:1039b76e893013e320cf9b54918d1e7ec80bfe314450035aa8d389693d46c3a5", size = 18400, upload-time = "2026-03-04T19:33:22.923Z" },
]
+[[package]]
+name = "outcome"
+version = "1.3.0.post0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060, upload-time = "2023-10-26T04:26:04.361Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692, upload-time = "2023-10-26T04:26:02.532Z" },
+]
+
[[package]]
name = "overturemaps"
version = "1.0.0"
@@ -2337,14 +3035,16 @@ name = "pandas"
version = "2.3.3"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
- "python_full_version >= '3.14' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
"python_full_version >= '3.14' and sys_platform == 'darwin'",
"python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
"python_full_version < '3.11' and sys_platform == 'darwin'",
"python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')",
+ "python_full_version < '3.11' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_machine != 'ARM64' and sys_platform == 'win32') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')",
]
dependencies = [
{ name = "numpy", marker = "python_full_version < '3.11' or python_full_version >= '3.14'" },
@@ -2408,8 +3108,10 @@ name = "pandas"
version = "3.0.3"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
- "python_full_version == '3.13.*' and sys_platform == 'win32'",
- "python_full_version == '3.12.*' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version == '3.13.*' and sys_platform == 'emscripten'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten'",
"python_full_version == '3.13.*' and sys_platform == 'darwin'",
@@ -2418,7 +3120,8 @@ resolution-markers = [
"python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
"(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
- "python_full_version == '3.11.*' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version == '3.11.*' and sys_platform == 'emscripten'",
"python_full_version == '3.11.*' and sys_platform == 'darwin'",
"python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
@@ -3225,7 +3928,8 @@ source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.11' and sys_platform == 'darwin'",
"python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')",
+ "python_full_version < '3.11' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_machine != 'ARM64' and sys_platform == 'win32') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')",
]
dependencies = [
{ name = "certifi", marker = "python_full_version < '3.11'" },
@@ -3271,13 +3975,16 @@ name = "pyproj"
version = "3.7.2"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
- "python_full_version >= '3.14' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
"python_full_version >= '3.14' and sys_platform == 'darwin'",
"python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
- "python_full_version == '3.13.*' and sys_platform == 'win32'",
- "python_full_version == '3.12.*' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version == '3.13.*' and sys_platform == 'emscripten'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten'",
"python_full_version == '3.13.*' and sys_platform == 'darwin'",
@@ -3286,7 +3993,8 @@ resolution-markers = [
"python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
"(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
- "python_full_version == '3.11.*' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version == '3.11.*' and sys_platform == 'emscripten'",
"python_full_version == '3.11.*' and sys_platform == 'darwin'",
"python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
@@ -3360,7 +4068,8 @@ source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.11' and sys_platform == 'darwin'",
"python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')",
+ "python_full_version < '3.11' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_machine != 'ARM64' and sys_platform == 'win32') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')",
]
dependencies = [
{ name = "configobj", marker = "python_full_version < '3.11'" },
@@ -3399,13 +4108,16 @@ name = "pyresample"
version = "1.35.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
- "python_full_version >= '3.14' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
"python_full_version >= '3.14' and sys_platform == 'darwin'",
"python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
- "python_full_version == '3.13.*' and sys_platform == 'win32'",
- "python_full_version == '3.12.*' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version == '3.13.*' and sys_platform == 'emscripten'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten'",
"python_full_version == '3.13.*' and sys_platform == 'darwin'",
@@ -3414,7 +4126,8 @@ resolution-markers = [
"python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
"(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
- "python_full_version == '3.11.*' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version == '3.11.*' and sys_platform == 'emscripten'",
"python_full_version == '3.11.*' and sys_platform == 'darwin'",
"python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
@@ -3623,10 +4336,14 @@ dependencies = [
{ name = "fsspec" },
{ name = "geopandas" },
{ name = "h3" },
+ { name = "h5netcdf" },
{ name = "h5py" },
{ name = "httpx" },
+ { name = "matplotlib" },
{ name = "msal" },
{ name = "nest-asyncio" },
+ { name = "netcdf4", version = "1.7.3", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'ARM64' and sys_platform == 'win32'" },
+ { name = "netcdf4", version = "1.7.4", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
{ name = "obstore" },
{ name = "osm2geojson" },
{ name = "overturemaps" },
@@ -3649,6 +4366,7 @@ dependencies = [
{ name = "s2cloudless" },
{ name = "satpy" },
{ name = "shapely" },
+ { name = "spacetrack" },
{ name = "sympy" },
{ name = "tensorflow", marker = "platform_machine == 'aarch64'" },
{ name = "tensorflow-cpu", marker = "platform_machine == 'x86_64'" },
@@ -3677,10 +4395,13 @@ requires-dist = [
{ name = "fsspec", specifier = ">=2026.4.0" },
{ name = "geopandas" },
{ name = "h3" },
+ { name = "h5netcdf", specifier = ">=1.8.1" },
{ name = "h5py", specifier = ">=3.16.0" },
{ name = "httpx" },
+ { name = "matplotlib", specifier = ">=3.10.9" },
{ name = "msal" },
{ name = "nest-asyncio" },
+ { name = "netcdf4", specifier = ">=1.7.3" },
{ name = "obstore", specifier = ">=0.9.5" },
{ name = "osm2geojson" },
{ name = "overturemaps" },
@@ -3703,6 +4424,7 @@ requires-dist = [
{ name = "s2cloudless" },
{ name = "satpy", specifier = ">=0.59.0" },
{ name = "shapely" },
+ { name = "spacetrack", specifier = ">=1.4.0" },
{ name = "sympy" },
{ name = "tensorflow", marker = "platform_machine == 'aarch64'", specifier = "==2.16.2" },
{ name = "tensorflow-cpu", marker = "platform_machine == 'x86_64'", specifier = "==2.16.2" },
@@ -3790,6 +4512,15 @@ 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 = "represent"
+version = "2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/27/1f/6a057fd4353ce17820681c776cfa2deeb3dc0eb9e5757e5c31ccc88b72ea/Represent-2.1.tar.gz", hash = "sha256:0b2d015c14e7ba6b3b5e6a7ba131a952013fe944339ac538764ce728a75dbcac", size = 19031, upload-time = "2024-01-23T17:54:19.821Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a2/60/9bb91d2a6ab018238a290d9244e61121a645bbf57c0b36b305a091e14ed0/Represent-2.1-py3-none-any.whl", hash = "sha256:94fd22d7fec378240c598b20b233f80545ec7eb1131076e2d3d759cee9be2588", size = 9000, upload-time = "2024-01-23T17:54:18.463Z" },
+]
+
[[package]]
name = "requests"
version = "2.34.2"
@@ -3838,7 +4569,8 @@ source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.11' and sys_platform == 'darwin'",
"python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')",
+ "python_full_version < '3.11' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_machine != 'ARM64' and sys_platform == 'win32') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')",
]
dependencies = [
{ name = "click", marker = "python_full_version < '3.11'" },
@@ -3856,13 +4588,16 @@ name = "rio-cogeo"
version = "7.0.2"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
- "python_full_version >= '3.14' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
"python_full_version >= '3.14' and sys_platform == 'darwin'",
"python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
- "python_full_version == '3.13.*' and sys_platform == 'win32'",
- "python_full_version == '3.12.*' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version == '3.13.*' and sys_platform == 'emscripten'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten'",
"python_full_version == '3.13.*' and sys_platform == 'darwin'",
@@ -3871,7 +4606,8 @@ resolution-markers = [
"python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
"(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
- "python_full_version == '3.11.*' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version == '3.11.*' and sys_platform == 'emscripten'",
"python_full_version == '3.11.*' and sys_platform == 'darwin'",
"python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
@@ -4010,6 +4746,18 @@ wheels = [
{ 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 = "rush"
+version = "2021.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ae/f8/3be2826ee081aeaf47f61bce24d56e4dbf32cb841a2f2708554d4b471ed4/rush-2021.4.0.tar.gz", hash = "sha256:818624075f0313f64a4c38ba62bc4a6526ee31b463990c8aebf03a98f5aaf264", size = 15378, upload-time = "2021-04-01T17:58:37.928Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1f/08/f38d9f6a3c6d9b5ea2a79414c9a65a7dd7bbe97ee1554396ffe1811a0fad/rush-2021.4.0-py3-none-any.whl", hash = "sha256:0f775f35a951b7874442c78eaa312bbd442be130dee9508f8d05b2f43dbd3acc", size = 18556, upload-time = "2021-04-01T17:58:36.628Z" },
+]
+
[[package]]
name = "s2cloudless"
version = "1.7.3"
@@ -4066,7 +4814,8 @@ source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.11' and sys_platform == 'darwin'",
"python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')",
+ "python_full_version < '3.11' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_machine != 'ARM64' and sys_platform == 'win32') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')",
]
dependencies = [
{ name = "numpy", marker = "python_full_version < '3.11'" },
@@ -4125,13 +4874,16 @@ name = "scipy"
version = "1.17.1"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
- "python_full_version >= '3.14' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
"python_full_version >= '3.14' and sys_platform == 'darwin'",
"python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
- "python_full_version == '3.13.*' and sys_platform == 'win32'",
- "python_full_version == '3.12.*' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version == '3.13.*' and sys_platform == 'emscripten'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten'",
"python_full_version == '3.13.*' and sys_platform == 'darwin'",
@@ -4140,7 +4892,8 @@ resolution-markers = [
"python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
"(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
- "python_full_version == '3.11.*' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version == '3.11.*' and sys_platform == 'emscripten'",
"python_full_version == '3.11.*' and sys_platform == 'darwin'",
"python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
@@ -4328,6 +5081,35 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
+[[package]]
+name = "sniffio"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
+]
+
+[[package]]
+name = "spacetrack"
+version = "1.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "filelock" },
+ { name = "httpx" },
+ { name = "logbook" },
+ { name = "outcome" },
+ { name = "platformdirs" },
+ { name = "python-dateutil", marker = "python_full_version < '3.11'" },
+ { name = "represent" },
+ { name = "rush" },
+ { name = "sniffio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0f/81/246faf20806cd112ee9f94102cf68f2a600c761f9fc83c36291b5da596a1/spacetrack-1.4.0.tar.gz", hash = "sha256:fe4b54c3dede0496a0ecc4039211481a25ae4fbd0eff5e54190441f0d4434c74", size = 44719, upload-time = "2025-02-24T13:17:13.363Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/86/91/7f6c8757549b80af2add458b00dbce72d57cbeff465398cf90836e108982/spacetrack-1.4.0-py3-none-any.whl", hash = "sha256:e693be354ee5a0b4f2205b2679049e0f709d8105c43d5a0ec0002e2f1a1d5e54", size = 17667, upload-time = "2025-02-24T13:17:11.801Z" },
+]
+
[[package]]
name = "sympy"
version = "1.14.0"
@@ -4345,15 +5127,15 @@ name = "tensorboard"
version = "2.16.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "absl-py" },
- { name = "grpcio" },
- { name = "markdown" },
- { name = "numpy" },
- { name = "protobuf" },
- { name = "setuptools" },
- { name = "six" },
- { name = "tensorboard-data-server" },
- { name = "werkzeug" },
+ { name = "absl-py", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "grpcio", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "markdown", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "numpy", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "protobuf", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "setuptools", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "six", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "tensorboard-data-server", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "werkzeug", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/d0/b97889ffa769e2d1fdebb632084d5e8b53fc299d43a537acee7ec0c021a3/tensorboard-2.16.2-py3-none-any.whl", hash = "sha256:9f2b4e7dad86667615c0e5cd072f1ea8403fc032a299f0072d6f74855775cc45", size = 5490335, upload-time = "2024-02-16T19:56:55.912Z" },
@@ -4374,29 +5156,29 @@ name = "tensorflow"
version = "2.16.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "absl-py" },
- { name = "astunparse" },
- { name = "flatbuffers" },
- { name = "gast" },
- { name = "google-pasta" },
- { name = "grpcio" },
- { name = "h5py" },
- { name = "keras", version = "3.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
- { name = "keras", version = "3.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
- { name = "libclang" },
- { name = "ml-dtypes" },
- { name = "numpy" },
- { name = "opt-einsum" },
- { name = "packaging" },
- { name = "protobuf" },
- { name = "requests" },
- { name = "setuptools" },
- { name = "six" },
- { name = "tensorboard" },
- { name = "tensorflow-io-gcs-filesystem", marker = "python_full_version < '3.12'" },
- { name = "termcolor" },
- { name = "typing-extensions" },
- { name = "wrapt" },
+ { name = "absl-py", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "astunparse", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "flatbuffers", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "gast", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "google-pasta", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "grpcio", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "h5py", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "keras", version = "3.12.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'ARM64') or (python_full_version < '3.11' and sys_platform != 'win32')" },
+ { name = "keras", version = "3.14.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'ARM64') or (python_full_version >= '3.11' and sys_platform != 'win32')" },
+ { name = "libclang", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "ml-dtypes", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "numpy", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "opt-einsum", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "packaging", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "protobuf", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "requests", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "setuptools", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "six", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "tensorboard", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "tensorflow-io-gcs-filesystem", marker = "(python_full_version < '3.12' and platform_machine != 'ARM64') or (python_full_version < '3.12' and sys_platform != 'win32')" },
+ { name = "termcolor", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "typing-extensions", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "wrapt", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/ea/49/0ba9a26146b93666d9d0a1207b0dbdff24caf96a2102dc1124aa1bcc0c5f/tensorflow-2.16.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:72c84f0e0f8ad0e7cb7b4b3fe9d1c899e6cbebc51c0e64df42a2a32a904aacd7", size = 226983658, upload-time = "2024-06-28T18:50:27.278Z" },
@@ -4412,29 +5194,29 @@ name = "tensorflow-cpu"
version = "2.16.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "absl-py", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" },
- { name = "astunparse", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" },
- { name = "flatbuffers", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" },
- { name = "gast", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" },
- { name = "google-pasta", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" },
- { name = "grpcio", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" },
- { name = "h5py", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" },
- { name = "keras", version = "3.12.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'aarch64') or (python_full_version < '3.11' and sys_platform != 'linux')" },
- { name = "keras", version = "3.14.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'aarch64') or (python_full_version >= '3.11' and sys_platform != 'linux')" },
- { name = "libclang", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" },
- { name = "ml-dtypes", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" },
- { name = "numpy", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" },
- { name = "opt-einsum", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" },
- { name = "packaging", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" },
- { name = "protobuf", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" },
- { name = "requests", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" },
- { name = "setuptools", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" },
- { name = "six", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" },
- { name = "tensorboard", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" },
- { name = "tensorflow-io-gcs-filesystem", marker = "(python_full_version < '3.12' and platform_machine != 'aarch64') or (python_full_version < '3.12' and sys_platform != 'linux')" },
- { name = "termcolor", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" },
- { name = "typing-extensions", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" },
- { name = "wrapt", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" },
+ { name = "absl-py", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (platform_machine != 'ARM64' and sys_platform == 'win32') or (sys_platform != 'linux' and sys_platform != 'win32')" },
+ { name = "astunparse", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (platform_machine != 'ARM64' and sys_platform == 'win32') or (sys_platform != 'linux' and sys_platform != 'win32')" },
+ { name = "flatbuffers", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (platform_machine != 'ARM64' and sys_platform == 'win32') or (sys_platform != 'linux' and sys_platform != 'win32')" },
+ { name = "gast", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (platform_machine != 'ARM64' and sys_platform == 'win32') or (sys_platform != 'linux' and sys_platform != 'win32')" },
+ { name = "google-pasta", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (platform_machine != 'ARM64' and sys_platform == 'win32') or (sys_platform != 'linux' and sys_platform != 'win32')" },
+ { name = "grpcio", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (platform_machine != 'ARM64' and sys_platform == 'win32') or (sys_platform != 'linux' and sys_platform != 'win32')" },
+ { name = "h5py", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (platform_machine != 'ARM64' and sys_platform == 'win32') or (sys_platform != 'linux' and sys_platform != 'win32')" },
+ { name = "keras", version = "3.12.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_machine != 'ARM64' and sys_platform == 'win32') or (python_full_version < '3.11' and sys_platform != 'linux' and sys_platform != 'win32')" },
+ { name = "keras", version = "3.14.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.11' and platform_machine != 'ARM64' and sys_platform == 'win32') or (python_full_version >= '3.11' and sys_platform != 'linux' and sys_platform != 'win32')" },
+ { name = "libclang", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (platform_machine != 'ARM64' and sys_platform == 'win32') or (sys_platform != 'linux' and sys_platform != 'win32')" },
+ { name = "ml-dtypes", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (platform_machine != 'ARM64' and sys_platform == 'win32') or (sys_platform != 'linux' and sys_platform != 'win32')" },
+ { name = "numpy", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (platform_machine != 'ARM64' and sys_platform == 'win32') or (sys_platform != 'linux' and sys_platform != 'win32')" },
+ { name = "opt-einsum", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (platform_machine != 'ARM64' and sys_platform == 'win32') or (sys_platform != 'linux' and sys_platform != 'win32')" },
+ { name = "packaging", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (platform_machine != 'ARM64' and sys_platform == 'win32') or (sys_platform != 'linux' and sys_platform != 'win32')" },
+ { name = "protobuf", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (platform_machine != 'ARM64' and sys_platform == 'win32') or (sys_platform != 'linux' and sys_platform != 'win32')" },
+ { name = "requests", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (platform_machine != 'ARM64' and sys_platform == 'win32') or (sys_platform != 'linux' and sys_platform != 'win32')" },
+ { name = "setuptools", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (platform_machine != 'ARM64' and sys_platform == 'win32') or (sys_platform != 'linux' and sys_platform != 'win32')" },
+ { name = "six", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (platform_machine != 'ARM64' and sys_platform == 'win32') or (sys_platform != 'linux' and sys_platform != 'win32')" },
+ { name = "tensorboard", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (platform_machine != 'ARM64' and sys_platform == 'win32') or (sys_platform != 'linux' and sys_platform != 'win32')" },
+ { name = "tensorflow-io-gcs-filesystem", marker = "(python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine != 'ARM64' and sys_platform == 'win32') or (python_full_version < '3.12' and sys_platform != 'linux' and sys_platform != 'win32')" },
+ { name = "termcolor", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (platform_machine != 'ARM64' and sys_platform == 'win32') or (sys_platform != 'linux' and sys_platform != 'win32')" },
+ { name = "typing-extensions", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (platform_machine != 'ARM64' and sys_platform == 'win32') or (sys_platform != 'linux' and sys_platform != 'win32')" },
+ { name = "wrapt", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (platform_machine != 'ARM64' and sys_platform == 'win32') or (sys_platform != 'linux' and sys_platform != 'win32')" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/96/c7/514f64b6e446b1161c30722aec00be1e5b4ae15ecdcb9ec4e3f6966a4314/tensorflow_cpu-2.16.2-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:fe1a52b0303486fc20b9832592f336c3851c41ca4a4cf9dfce1616f0c60a40c2", size = 259544880, upload-time = "2024-06-28T18:53:46.157Z" },
@@ -4483,7 +5265,8 @@ source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.11' and sys_platform == 'darwin'",
"python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')",
+ "python_full_version < '3.11' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_machine != 'ARM64' and sys_platform == 'win32') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')",
]
dependencies = [
{ name = "numpy", marker = "python_full_version < '3.11'" },
@@ -4498,13 +5281,16 @@ name = "tifffile"
version = "2026.3.3"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
- "python_full_version >= '3.14' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
"python_full_version >= '3.14' and sys_platform == 'darwin'",
"python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
- "python_full_version == '3.13.*' and sys_platform == 'win32'",
- "python_full_version == '3.12.*' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version == '3.13.*' and sys_platform == 'emscripten'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten'",
"python_full_version == '3.13.*' and sys_platform == 'darwin'",
@@ -4513,7 +5299,8 @@ resolution-markers = [
"python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
"(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
- "python_full_version == '3.11.*' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version == '3.11.*' and sys_platform == 'emscripten'",
"python_full_version == '3.11.*' and sys_platform == 'darwin'",
"python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
@@ -4618,7 +5405,8 @@ source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.11' and sys_platform == 'darwin'",
"python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')",
+ "python_full_version < '3.11' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_machine != 'ARM64' and sys_platform == 'win32') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')",
]
dependencies = [
{ name = "numpy", marker = "python_full_version < '3.11'" },
@@ -4661,13 +5449,16 @@ name = "trollimage"
version = "1.28.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
- "python_full_version >= '3.14' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
"python_full_version >= '3.14' and sys_platform == 'darwin'",
"python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
- "python_full_version == '3.13.*' and sys_platform == 'win32'",
- "python_full_version == '3.12.*' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version == '3.13.*' and sys_platform == 'emscripten'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten'",
"python_full_version == '3.13.*' and sys_platform == 'darwin'",
@@ -4676,7 +5467,8 @@ resolution-markers = [
"python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
"(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
- "python_full_version == '3.11.*' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version == '3.11.*' and sys_platform == 'emscripten'",
"python_full_version == '3.11.*' and sys_platform == 'darwin'",
"python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
@@ -4864,7 +5656,7 @@ name = "werkzeug"
version = "3.1.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "markupsafe" },
+ { name = "markupsafe", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" }
wheels = [
@@ -4876,7 +5668,7 @@ name = "wheel"
version = "0.47.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "packaging" },
+ { name = "packaging", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/39/62/75f18a0f03b4219c456652c7780e4d749b929eb605c098ce3a5b6b6bc081/wheel-0.47.0.tar.gz", hash = "sha256:cc72bd1009ba0cf63922e28f94d9d83b920aa2bb28f798a31d0691b02fa3c9b3", size = 63854, upload-time = "2026-04-22T15:51:27.727Z" }
wheels = [
@@ -4969,7 +5761,8 @@ source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.11' and sys_platform == 'darwin'",
"python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')",
+ "python_full_version < '3.11' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_machine != 'ARM64' and sys_platform == 'win32') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')",
]
dependencies = [
{ name = "numpy", marker = "python_full_version < '3.11'" },
@@ -4986,13 +5779,16 @@ name = "xarray"
version = "2026.4.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
- "python_full_version >= '3.14' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
"python_full_version >= '3.14' and sys_platform == 'darwin'",
"python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
- "python_full_version == '3.13.*' and sys_platform == 'win32'",
- "python_full_version == '3.12.*' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version == '3.13.*' and sys_platform == 'emscripten'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten'",
"python_full_version == '3.13.*' and sys_platform == 'darwin'",
@@ -5001,7 +5797,8 @@ resolution-markers = [
"python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
"(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
- "python_full_version == '3.11.*' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version == '3.11.*' and sys_platform == 'emscripten'",
"python_full_version == '3.11.*' and sys_platform == 'darwin'",
"python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
@@ -5165,7 +5962,8 @@ source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.11' and sys_platform == 'darwin'",
"python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')",
+ "python_full_version < '3.11' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_machine != 'ARM64' and sys_platform == 'win32') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')",
]
dependencies = [
{ name = "asciitree", marker = "python_full_version < '3.11'" },
@@ -5183,13 +5981,16 @@ name = "zarr"
version = "3.1.5"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
- "python_full_version >= '3.14' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
"python_full_version >= '3.14' and sys_platform == 'darwin'",
"python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
- "python_full_version == '3.13.*' and sys_platform == 'win32'",
- "python_full_version == '3.12.*' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version == '3.13.*' and sys_platform == 'emscripten'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten'",
"python_full_version == '3.13.*' and sys_platform == 'darwin'",
@@ -5198,7 +5999,8 @@ resolution-markers = [
"python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
"(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
- "python_full_version == '3.11.*' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine == 'ARM64' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and platform_machine != 'ARM64' and sys_platform == 'win32'",
"python_full_version == '3.11.*' and sys_platform == 'emscripten'",
"python_full_version == '3.11.*' and sys_platform == 'darwin'",
"python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",