From fd27b1bbb1f60bde3589ef3ef9831ef30e943ed9 Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Fri, 12 Dec 2025 17:13:43 +0000 Subject: [PATCH 01/50] first pass on handling new zarr v3 store with new image metadata format --- packages/core/src/schemas/index.ts | 87 +++++++++++++- packages/zarrextra/src/index.ts | 177 +++++++++++++++++++++++++---- packages/zarrextra/src/types.ts | 36 +++++- 3 files changed, 274 insertions(+), 26 deletions(-) diff --git a/packages/core/src/schemas/index.ts b/packages/core/src/schemas/index.ts index c962b40..cd597a8 100644 --- a/packages/core/src/schemas/index.ts +++ b/packages/core/src/schemas/index.ts @@ -255,10 +255,10 @@ export const spatialDataAttrsSchema = z export type SpatialDataAttrs = z.infer; /** - * Schema for raster element attrs (images & labels) - * Combines OME-NGFF multiscales with spatialdata_attrs + * Schema for raster element attrs in spatialdata 0.5.0 format + * Has multiscales at the top level (older OME-NGFF format) */ -export const rasterAttrsSchema = z +const rasterAttrsV050Schema = z .object({ multiscales: z .array( @@ -282,7 +282,86 @@ export const rasterAttrsSchema = z }) .passthrough(); -export type RasterAttrs = z.infer; +/** + * Schema for raster element attrs in spatialdata 0.6.1+ format + * Has multiscales nested under 'ome' key (newer OME-NGFF format) + */ +const rasterAttrsV061Schema = z + .object({ + ome: z + .object({ + multiscales: z + .array( + z.object({ + name: z.string().optional(), + datasets: z + .array( + z.object({ + path: z.string(), + coordinateTransformations: coordinateTransformationSchema.optional(), + }) + ) + .min(1), + axes: axesSchema, + coordinateTransformations: coordinateTransformationSchema.optional(), + }) + ) + .min(1), + omero: omeroSchema.optional(), + }) + .passthrough(), + spatialdata_attrs: spatialDataAttrsSchema.optional(), + }) + .passthrough(); + +/** + * Schema for raster element attrs (images & labels) + * Supports both spatialdata 0.5.0 (top-level multiscales) and 0.6.1+ (nested under 'ome') formats. + * Uses zod transform to normalize both formats to a consistent internal representation. + */ +export const rasterAttrsSchema = z + .union([rasterAttrsV050Schema, rasterAttrsV061Schema]) + .transform((data): RasterAttrs => { + // If it's the v0.6.1+ format (has 'ome' key), extract multiscales and omero from it + if ('ome' in data && data.ome && typeof data.ome === 'object') { + const omeData = data.ome as { + multiscales: unknown; + omero?: unknown; + [key: string]: unknown; + }; + return { + multiscales: omeData.multiscales as RasterAttrs['multiscales'], + omero: omeData.omero as RasterAttrs['omero'], + spatialdata_attrs: 'spatialdata_attrs' in data ? data.spatialdata_attrs : undefined, + // Preserve any other top-level fields + ...Object.fromEntries( + Object.entries(data).filter(([key]) => key !== 'ome' && key !== 'spatialdata_attrs') + ), + } as RasterAttrs; + } + + // Otherwise, it's already in the v0.5.0 format (top-level multiscales), return as-is + return data as RasterAttrs; + }); + +/** + * Internal type for the normalized raster attrs structure + * This is what we use internally after transformation + */ +export type RasterAttrs = { + multiscales: Array<{ + name?: string; + datasets: Array<{ + path: string; + coordinateTransformations?: z.infer; + }>; + axes: z.infer; + coordinateTransformations?: z.infer; + }>; + omero?: z.infer; + spatialdata_attrs?: z.infer; + [key: string]: unknown; +}; /** * Schema for shapes element attrs. diff --git a/packages/zarrextra/src/index.ts b/packages/zarrextra/src/index.ts index 9f6fa69..cfcdb63 100644 --- a/packages/zarrextra/src/index.ts +++ b/packages/zarrextra/src/index.ts @@ -1,5 +1,5 @@ import * as zarr from 'zarrita'; -import type { ZarrTree, ConsolidatedStore, ZAttrsAny, IntermediateConsolidatedStore } from './types'; +import type { ZarrTree, ConsolidatedStore, ZAttrsAny, IntermediateConsolidatedStore, ZarrV2Metadata, ZarrV3Metadata } from './types'; import { ATTRS_KEY, ZARRAY_KEY } from './types'; import { Err, Ok, type Result } from './result'; @@ -49,40 +49,160 @@ export async function parseStoreContents(store: IntermediateConsolidatedStore): return tree; } +/** + * Parse zarr v3 consolidated metadata from zarr.json + * In zarr v3, consolidated metadata may have a different structure than v2. + * This function normalizes it to a format compatible with our getZattrs function. + */ +async function parseZarrJson(zarrJson: unknown): Promise> { + if (!zarrJson || typeof zarrJson !== 'object') { + return Err(new Error(`Invalid zarr.json: expected an object but got ${typeof zarrJson}`)); + } + + // zarr v3 zarr.json structure can vary, but typically has metadata nested by path + const parsed = zarrJson as ZarrV3Metadata; + + // If it already has the expected structure, return as-is + if (parsed.metadata && typeof parsed.metadata === 'object') { + return Ok(parsed); + } + + // Otherwise, try to normalize the structure + // This is a fallback for different zarr v3 implementations + const normalized: ZarrV3Metadata = { + metadata: parsed.metadata || {}, + ...parsed + }; + + if (!normalized.metadata || typeof normalized.metadata !== 'object') { + return Err(new Error('Invalid zarr.json: metadata field is missing or not an object')); + } + + return Ok(normalized); +} + +/** + * Normalize zarr v2 flat metadata to nested structure like v3 + * Converts flat structure like { "path/.zattrs": {...} } + * to nested structure like { "path": { ".zattrs": {...} } } + */ +function normalizeV2ToV3Metadata(v2Metadata: ZarrV2Metadata): ZarrV3Metadata { + const nested: ZarrV3Metadata['metadata'] = {}; + + if (!v2Metadata.metadata || typeof v2Metadata.metadata !== 'object') { + return { metadata: {} }; + } + + // Iterate through flat path keys in v2 metadata + for (const [flatPath, value] of Object.entries(v2Metadata.metadata)) { + // Match patterns like "path/.zattrs", "path/.zarray", "path/.zgroup" + const match = flatPath.match(/^(.+?)\/(\.zattrs|\.zarray|\.zgroup)$/); + if (match) { + const [, path, metadataType] = match; + if (!nested[path]) { + nested[path] = {}; + } + nested[path][metadataType as '.zattrs' | '.zarray' | '.zgroup'] = value; + } + } + + return { metadata: nested }; +} + // we might not always use the FetchStore, this is for convenience & could change /** - * There is a tendency for .zmetadata to be misnamed as zmetadata... + * Try to open consolidated metadata from a zarr store. + * Supports both zarr v2 (.zmetadata) and v3 (zarr.json) formats. + * There is a tendency for .zmetadata to be misnamed as zmetadata in v2. */ export async function tryConsolidated(store: zarr.FetchStore): Promise { - // in future, first we'll try zarr.json - // and I'm sure we can make this implementation less ugly, kinda trivial though so cba for now. //!!! nb - we need to also handle local files, in which case we don't fetch(url), we need another method - this is important + + // First, try zarr.json (v3 format) + try { + const zarrJsonPath = `${store.url}/zarr.json`; + const zarrJson = await (await fetch(zarrJsonPath)).json(); + const parseResult = await parseZarrJson(zarrJson); + + if (!parseResult.ok) { + // Fall through to try v2 formats + throw parseResult.error; + } + + const v3Metadata = parseResult.value; + + // For v3, zarrita's withConsolidated doesn't work, so we need to implement our own contents() + // Extract paths from the nested metadata structure + const paths = new Set(); + if (v3Metadata.metadata && typeof v3Metadata.metadata === 'object') { + for (const path of Object.keys(v3Metadata.metadata)) { + paths.add(path); + } + } + const uniquePaths = Array.from(paths); + + // For v3, we need to create a listable store + // Since zarrita's withConsolidated doesn't work for v3, we create our own wrapper + // that implements contents() based on the metadata we parsed + const v3ListableStore = Object.assign(store, { + contents: () => { + // Return contents based on metadata paths + return uniquePaths.map(p => { + const fullPath = p.startsWith('/') ? p : `/${p}`; + const pathMetadata = v3Metadata.metadata?.[p]; + return { + path: fullPath as `/${string}`, + kind: pathMetadata && '.zarray' in pathMetadata ? ('array' as const) : ('group' as const) + }; + }); + } + }) as zarr.Listable; + + return { + ...v3ListableStore, + zmetadata: v3Metadata, + metadataFormat: 'v3' + }; + } catch { + // Fall through to try v2 formats + } + + // Try .zmetadata (v2 format) try { const path = `${store.url}/.zmetadata`; - // is there a zod schema we could be using here? - const zmetadata = await (await fetch(path)).json(); + const zmetadata = await (await fetch(path)).json() as ZarrV2Metadata; const zarrita = await zarr.withConsolidated(store); - return { ...zarrita, zmetadata } + // Normalize v2 flat metadata to v3 nested format for consistent internal use + const v3Metadata = normalizeV2ToV3Metadata(zmetadata); + return { + ...zarrita, + zmetadata: v3Metadata, + metadataFormat: 'v3' // Store as v3 format after normalization + }; } catch { + // Try zmetadata (v2 variant, misnamed) try { const path = `${store.url}/zmetadata`; - const zmetadata = await (await fetch(path)).json(); + const zmetadata = await (await fetch(path)).json() as ZarrV2Metadata; const zarrita = await zarr.withConsolidated(store, { metadataKey: 'zmetadata' }); - return { ...zarrita, zmetadata } + // Normalize v2 flat metadata to v3 nested format for consistent internal use + const v3Metadata = normalizeV2ToV3Metadata(zmetadata); + return { + ...zarrita, + zmetadata: v3Metadata, + metadataFormat: 'v3' // Store as v3 format after normalization + }; } catch { - throw new Error(`Couldn't open consolidated metadata for '${store.url}' - n.b. zarr v3 / spatialdata >0.5 is not supported yet`); + throw new Error( + `Couldn't open consolidated metadata for '${store.url}'. Tried: zarr.json (v3), .zmetadata (v2), and zmetadata (v2 variant). Ensure the store has consolidated metadata enabled.` + ); } } - - // nb for now we explicitly only support consolidated store, so if it doesn't find either key this is an error - // --- also note, as of writing zarrita doesn't support consolidated metadata on v3 stores - // - meaning in that case we might not use its `withConsolidated` function at all (especially since it's IMO not as useful as it should be even for v2), - // so we should refactor our parsing to not use `ListableStore.contents()` and probably avoid the extra fetch. - // return zarr.withConsolidated(store).catch(() => zarr.withConsolidated(store, { metadataKey: 'zmetadata' })); } /** * Try to open a consolidated `zarr` store and return a `Result`, + * Supports both zarr v2 and v3 formats. */ export async function openExtraConsolidated(source: string): Promise> { // could `source` also be a File or something? @@ -90,24 +210,39 @@ export async function openExtraConsolidated(source: string): Promise | undefined> { - const attrPath = `${path}/${k}`.slice(1); - const attr = store.zmetadata.metadata[attrPath]; //may be undefined, that's fine. + // All metadata is stored in v3 nested format after normalization + const v3Metadata = store.zmetadata as ZarrV3Metadata; + const pathStr = path.slice(1); // Remove leading '/' + const pathMetadata = v3Metadata.metadata?.[pathStr]; + if (!pathMetadata || typeof pathMetadata !== 'object') { + return undefined; + } + + const attr = pathMetadata[k as '.zattrs' | '.zarray' | '.zgroup']; if (!attr) return undefined; - return attr; + return attr as Record; } /** diff --git a/packages/zarrextra/src/types.ts b/packages/zarrextra/src/types.ts index b9c8c68..806df66 100644 --- a/packages/zarrextra/src/types.ts +++ b/packages/zarrextra/src/types.ts @@ -42,10 +42,44 @@ export interface ZarrTree { [key: string]: ZarrTree | LazyZarrArray; } +/** + * Zarr v2 consolidated metadata structure (.zmetadata) + * Has a flat metadata object with path keys like "path/.zattrs" + */ +export type ZarrV2Metadata = { + metadata: Record; +}; + +/** + * Zarr v3 consolidated metadata structure (zarr.json) + * Structure may vary, but typically has nested metadata per path + */ +export type ZarrV3Metadata = { + metadata?: Record; + // May have other fields depending on zarr v3 spec + [key: string]: unknown; +}; + +/** + * Union type for consolidated metadata (v2 or v3) + */ +export type ConsolidatedMetadata = ZarrV2Metadata | ZarrV3Metadata; + +/** + * Discriminated union to identify metadata format + */ +export type MetadataFormat = + | { format: 'v2'; metadata: ZarrV2Metadata } + | { format: 'v3'; metadata: ZarrV3Metadata }; + /** * A zarrita store with the raw metadata appended as `zmetadata` - mostly for internal use and subject to revision. + * The zmetadata can be either v2 or v3 format, and we track which format it is. */ -export type IntermediateConsolidatedStore = zarr.Listable & { zmetadata: any }; +export type IntermediateConsolidatedStore = zarr.Listable & { + zmetadata: ConsolidatedMetadata; + metadataFormat: 'v2' | 'v3'; +}; /** * This type is liable to change in future - for now, it has `zarritaStore` which is the `ListableStore` from `zarrita`, * and `tree: ZarrTree` which has the object hierarchy as described in the consolidated metadata as a mostly "Plain Old Javascript Object", From dd9fea12e7e8a30e14193159ef385027e2da760c Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Fri, 12 Dec 2025 17:40:44 +0000 Subject: [PATCH 02/50] normalise to new versions of metadata, don't use zarrita Listable --- packages/zarrextra/src/index.ts | 145 ++++++++++++++++---------------- packages/zarrextra/src/types.ts | 11 +-- 2 files changed, 80 insertions(+), 76 deletions(-) diff --git a/packages/zarrextra/src/index.ts b/packages/zarrextra/src/index.ts index cfcdb63..589c530 100644 --- a/packages/zarrextra/src/index.ts +++ b/packages/zarrextra/src/index.ts @@ -8,37 +8,71 @@ import { Err, Ok, type Result } from './result'; * * This traverses arbitrary group depth etc - handy for a generic zarr thing, but for SpatialData we can have * something more explicitly targetting the expected structure. + * + * Works directly with the normalized v3 metadata structure, extracting paths from metadata rather than + * relying on store.contents(). */ -export async function parseStoreContents(store: IntermediateConsolidatedStore): Promise { - // this can await get metadata without too much issue given we know it's already there... +async function parseStoreContents(store: IntermediateConsolidatedStore): Promise { + // All metadata is normalized to v3 nested format + const metadata = store.zmetadata.metadata; + + if (!metadata || typeof metadata !== 'object') { + return {}; + } + + // Extract all paths from metadata and determine their types + const pathInfo = new Map(); + + for (const [path, pathMetadata] of Object.entries(metadata)) { + if (pathMetadata && typeof pathMetadata === 'object') { + const isArray = '.zarray' in pathMetadata; + pathInfo.set(path, { isArray, path }); + } + } + + // Sort paths by depth (shorter paths first) to build tree top-down + const sortedPaths = Array.from(pathInfo.entries()).sort((a, b) => { + const depthA = a[0].split('/').filter(p => p).length; + const depthB = b[0].split('/').filter(p => p).length; + return depthA - depthB; + }); + + // Open root for resolving array paths later const root = await zarr.open(store, { kind: 'group' }); - const contents = store.contents().map(v => { - const pathParts = v.path.split('/'); - // might do something with the top-level element name - ie, make a different kind of object for each - - const path = pathParts.slice(1); - return { path, kind: v.kind, v }; - }).sort((a, b) => a.path.length - b.path.length).slice(1); // skip the root group itself - - + const tree: ZarrTree = {}; - for (const item of contents) { + + for (const [fullPath, info] of sortedPaths) { + // Skip root path + if (!fullPath || fullPath === '/' || fullPath === '') continue; + + // Normalize path: ensure it starts with / and doesn't end with / + const normalizedPath = fullPath.startsWith('/') ? fullPath : `/${fullPath}`; + const pathParts = normalizedPath.split('/').filter(p => p); + + if (pathParts.length === 0) continue; + let currentNode = tree; - for (const [i, part] of item.path.entries()) { + + // Build tree structure + for (const [i, part] of pathParts.entries()) { if (!(part in currentNode)) { - const leaf = (i === (item.path.length - 1)) && item.kind === "array"; - // get zattrs... says it's `async` but I strongly don't want it to actually be fetching unncessarily. - // current implementation will use the existing zmetadata and will need to be adapted to zarr.json in v3 - const attrs = await getZattrs(item.v.path, store); - if (leaf) { - const zarray = await getZattrs(item.v.path, store, ".zarray"); - // I suppose this could cache itself as well, but I'm not sure this is really for actual use + const isLeaf = i === pathParts.length - 1; + const isArray = isLeaf && info.isArray; + + // Get attributes for this path (normalizedPath already has leading /) + const attrs = await getZattrs(normalizedPath as zarr.AbsolutePath, store); + + if (isArray) { + // Leaf array node + const zarray = await getZattrs(normalizedPath as zarr.AbsolutePath, store, ".zarray"); currentNode[part] = { [ATTRS_KEY]: attrs, [ZARRAY_KEY]: zarray ?? ({} as ZAttrsAny), - get: () => zarr.open(root.resolve(item.v.path), { kind: 'array' }) + get: () => zarr.open(root.resolve(normalizedPath), { kind: 'array' }) }; } else { + // Group node currentNode[part] = { [ATTRS_KEY]: attrs }; } } @@ -46,6 +80,7 @@ export async function parseStoreContents(store: IntermediateConsolidatedStore): currentNode = currentNode[part] as ZarrTree; } } + return tree; } @@ -114,8 +149,11 @@ function normalizeV2ToV3Metadata(v2Metadata: ZarrV2Metadata): ZarrV3Metadata { * Try to open consolidated metadata from a zarr store. * Supports both zarr v2 (.zmetadata) and v3 (zarr.json) formats. * There is a tendency for .zmetadata to be misnamed as zmetadata in v2. + * + * Since we work directly with metadata and don't use contents(), we don't need + * zarrita's withConsolidated() - we just fetch the metadata files and normalize them. */ -export async function tryConsolidated(store: zarr.FetchStore): Promise { +async function tryConsolidated(store: zarr.FetchStore): Promise { //!!! nb - we need to also handle local files, in which case we don't fetch(url), we need another method - this is important // First, try zarr.json (v3 format) @@ -131,38 +169,10 @@ export async function tryConsolidated(store: zarr.FetchStore): Promise(); - if (v3Metadata.metadata && typeof v3Metadata.metadata === 'object') { - for (const path of Object.keys(v3Metadata.metadata)) { - paths.add(path); - } - } - const uniquePaths = Array.from(paths); - - // For v3, we need to create a listable store - // Since zarrita's withConsolidated doesn't work for v3, we create our own wrapper - // that implements contents() based on the metadata we parsed - const v3ListableStore = Object.assign(store, { - contents: () => { - // Return contents based on metadata paths - return uniquePaths.map(p => { - const fullPath = p.startsWith('/') ? p : `/${p}`; - const pathMetadata = v3Metadata.metadata?.[p]; - return { - path: fullPath as `/${string}`, - kind: pathMetadata && '.zarray' in pathMetadata ? ('array' as const) : ('group' as const) - }; - }); - } - }) as zarr.Listable; - - return { - ...v3ListableStore, - zmetadata: v3Metadata, - metadataFormat: 'v3' - }; + // Return store with metadata properties + return Object.assign(store, { + zmetadata: v3Metadata + }); } catch { // Fall through to try v2 formats } @@ -171,27 +181,24 @@ export async function tryConsolidated(store: zarr.FetchStore): Promise | undefined> { - // All metadata is stored in v3 nested format after normalization - const v3Metadata = store.zmetadata as ZarrV3Metadata; const pathStr = path.slice(1); // Remove leading '/' - const pathMetadata = v3Metadata.metadata?.[pathStr]; + const pathMetadata = store.zmetadata.metadata?.[pathStr]; if (!pathMetadata || typeof pathMetadata !== 'object') { return undefined; } diff --git a/packages/zarrextra/src/types.ts b/packages/zarrextra/src/types.ts index 806df66..6b51a50 100644 --- a/packages/zarrextra/src/types.ts +++ b/packages/zarrextra/src/types.ts @@ -73,12 +73,13 @@ export type MetadataFormat = | { format: 'v3'; metadata: ZarrV3Metadata }; /** - * A zarrita store with the raw metadata appended as `zmetadata` - mostly for internal use and subject to revision. - * The zmetadata can be either v2 or v3 format, and we track which format it is. + * A zarrita store with metadata appended as `zmetadata` - mostly for internal use and subject to revision. + * All metadata is normalized to v3 nested format internally, regardless of the original format (v2 or v3). + * Uses `Store` (FetchStore) which already implements `Readable` - we work directly with metadata + * and don't need `contents()` from `Listable`. */ -export type IntermediateConsolidatedStore = zarr.Listable & { - zmetadata: ConsolidatedMetadata; - metadataFormat: 'v2' | 'v3'; +export type IntermediateConsolidatedStore = Store & { + zmetadata: ZarrV3Metadata; }; /** * This type is liable to change in future - for now, it has `zarritaStore` which is the `ListableStore` from `zarrita`, From d733f211c44d2d7c9969ee7bf467e781cdf40d8f Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Fri, 12 Dec 2025 22:06:09 +0000 Subject: [PATCH 03/50] omero channel label allows number --- packages/core/src/schemas/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/schemas/index.ts b/packages/core/src/schemas/index.ts index cd597a8..0d39648 100644 --- a/packages/core/src/schemas/index.ts +++ b/packages/core/src/schemas/index.ts @@ -202,7 +202,7 @@ const omeroSchema = z.object({ start: z.number(), }) .optional(), - label: z.string().optional(), + label: z.union([z.number(), z.string()]).optional(), family: z.string().optional(), color: z.string().optional(), active: z.boolean().optional(), From 3be2f49f0a8bbd63006d9f77cd482fffaf343f83 Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Fri, 12 Dec 2025 22:38:44 +0000 Subject: [PATCH 04/50] fix v3 metadata, refactor some types, treat schema errors as errors --- packages/core/src/models/index.ts | 62 +++++------ packages/core/src/store/index.ts | 65 +++++------ packages/zarrextra/src/index.ts | 176 ++++++++++++++++++++++-------- packages/zarrextra/src/types.ts | 62 ++++++++++- 4 files changed, 253 insertions(+), 112 deletions(-) diff --git a/packages/core/src/models/index.ts b/packages/core/src/models/index.ts index dad2edd..98440d3 100644 --- a/packages/core/src/models/index.ts +++ b/packages/core/src/models/index.ts @@ -54,14 +54,14 @@ abstract class AbstractElement { this.key = key; this.url = `${sdata.url}/${name}/${key}`; - const { parsed } = sdata; - if (!parsed) { - throw new Error("Parsed store contents not available"); + const { tree } = sdata.rootStore; + if (!tree) { + throw new Error("Tree store contents not available"); } - if (!(name in parsed)) { + if (!(name in tree)) { throw new Error(`Unknown element type: ${name}`); } - const p1 = parsed[name] as ZarrTree; + const p1 = tree[name] as ZarrTree; if (!(key in p1)) { throw new Error(`Unknown element key: ${key}`); } @@ -205,11 +205,9 @@ export class TableElement extends AbstractElement<'tables'> { // parse attrs through schema const result = tableAttrsSchema.safeParse(this.rawAttrs); if (!result.success) { - console.warn(`Schema validation failed for ${params.name}/${params.key}:`, result.error.issues); - this.attrs = this.rawAttrs as TableAttrs; - } else { - this.attrs = result.data; - } + throw result.error; + } + this.attrs = result.data; } /** @@ -237,12 +235,9 @@ abstract class RasterElement extends AbstractSpat // Parse attrs through schema const result = rasterAttrsSchema.safeParse(this.rawAttrs); if (!result.success) { - console.warn(`Schema validation failed for ${params.name}/${params.key}:`, result.error.issues); - // Fall back to raw attrs cast - allows working with non-conformant data - this.attrs = this.rawAttrs as RasterAttrs; - } else { - this.attrs = result.data; + throw result.error; } + this.attrs = result.data; } /** @@ -338,15 +333,13 @@ export class ShapesElement extends AbstractSpatialElement<'shapes', ShapesAttrs> // Parse attrs through schema const result = shapesAttrsSchema.safeParse(this.rawAttrs); if (!result.success) { - console.warn(`Schema validation failed for shapes/${params.key}:`, result.error.issues); - this.attrs = this.rawAttrs as ShapesAttrs; - } else { - this.attrs = result.data; - } + throw result.error; + } + this.attrs = result.data; // Initialize the Vitessce-derived shapes source for loading geometry this.vShapes = new SpatialDataShapesSource({ - store: params.sdata.rootStore, + store: params.sdata.rootStore.zarritaStore, fileType: '.zarr' }); } @@ -396,13 +389,11 @@ export class PointsElement extends AbstractSpatialElement<'points', PointsAttrs> // Parse attrs through schema const result = pointsAttrsSchema.safeParse(this.rawAttrs); if (!result.success) { - console.warn(`Schema validation failed for points/${params.key}:`, result.error.issues); - this.attrs = this.rawAttrs as PointsAttrs; - } else { - this.attrs = result.data; + throw result.error; } + this.attrs = result.data; this.vPoints = new SpatialDataPointsSource({ - store: params.sdata.rootStore, + store: params.sdata.rootStore.zarritaStore, fileType: '.zarr' }); } @@ -486,22 +477,29 @@ export function loadElements( sdata: SDataProps, name: T ): Record | undefined { - const { parsed } = sdata; - if (!parsed) { - throw new Error("Parsed store contents not available"); + const { tree } = sdata.rootStore; + if (!tree) { + throw new Error("Tree store contents not available"); } - if (!(name in parsed)) { + if (!(name in tree)) { return undefined; } - const keys = Object.keys(parsed[name] as object); + const keys = Object.keys(tree[name] as object); if (keys.length === 0) { return undefined; } const result: Record = {}; for (const key of keys) { - result[key] = createElement(name, sdata, key); + try { + result[key] = createElement(name, sdata, key); + } catch (error) { + // we might want to take this more seriously - but this should prevent any crash + // and not have any worse symptom than the element not being there as a result(?) + // might be slightly confusing to get {} rather than undefined in that case + console.error(error); + } } return result; } diff --git a/packages/core/src/store/index.ts b/packages/core/src/store/index.ts index a1a2b73..1d0a80a 100644 --- a/packages/core/src/store/index.ts +++ b/packages/core/src/store/index.ts @@ -25,7 +25,7 @@ export type { SpatialElement, AnyElement } from '../models'; export class SpatialData { readonly url: StoreLocation; - rootStore: zarr.Listable; + rootStore: ConsolidatedStore; // metadata: Record; //todo: add this, with type (validated by zod) images?: Elements<'images'>; @@ -34,15 +34,9 @@ export class SpatialData { shapes?: Elements<'shapes'>; tables?: Elements<'tables'>; - /** - * Keeping this for experimenting with this structure vs AnnData.js for Tables etc. - */ - parsed?: ZarrTree; - constructor(url: StoreLocation, rootStore: ConsolidatedStore, selection?: ElementName[], onBadFiles?: BadFileHandler) { this.url = url; - this.rootStore = rootStore.zarritaStore; - this.parsed = rootStore.tree; + this.rootStore = rootStore; const _selection = selection || ElementNames; for (const elementType of _selection) { // Load all elements of this type @@ -86,33 +80,42 @@ export class SpatialData { * to get more detailed info. */ toString() { - const nonEmptyElements = ElementNames.filter((name) => this[name] !== undefined); - if (nonEmptyElements.length === 0) { - return `SpatialData object, with associated Zarr store: ${this.url}\n(No elements loaded)`; - } - const elements = nonEmptyElements.map((name, i) => { - const element = this[name]; - const isLast = i === nonEmptyElements.length - 1; - const prefix = isLast ? '└──' : '├──'; - const childPrefix = isLast ? ' ' : '│ '; - if (element) { - const keys = Object.keys(element); - const children = keys.map((key, j) => { - const childIsLast = j === keys.length - 1; - const childBranch = childIsLast ? '└──' : '├──'; - return `${childPrefix}${childBranch} ${key}`; - }).join('\n'); - return `${prefix} ${name}:\n${children}`; + try { + const nonEmptyElements = ElementNames.filter((name) => this[name] !== undefined); + if (nonEmptyElements.length === 0) { + return `SpatialData object, with associated Zarr store: ${this.url}\n(No elements loaded)`; } - return `${prefix} ${name}: (empty)`; - }).join('\n'); - const cs = `with coordinate systems: ${this.coordinateSystems.join(', ')}`; - return `SpatialData object, with associated Zarr store: ${this.url}\nElements:\n${elements}\n${cs}`; + const elements = nonEmptyElements.map((name, i) => { + const element = this[name]; + const isLast = i === nonEmptyElements.length - 1; + const prefix = isLast ? '└──' : '├──'; + const childPrefix = isLast ? ' ' : '│ '; + if (element) { + const keys = Object.keys(element); + const children = keys.map((key, j) => { + const childIsLast = j === keys.length - 1; + const childBranch = childIsLast ? '└──' : '├──'; + return `${childPrefix}${childBranch} ${key}`; + }).join('\n'); + return `${prefix} ${name}:\n${children}`; + } + return `${prefix} ${name}: (empty)`; + }).join('\n'); + const cs = `with coordinate systems: ${this.coordinateSystems.join(', ')}`; + return `SpatialData object, with associated Zarr store: ${this.url}\nElements:\n${elements}\n${cs}`; + } catch (error) { + // this can happen if `this.coordinateSystems` trips over some invalid `attrs` where we did a bad `as Whatever` after validation fails... + // without the catch we get a really nasty crash. + // nb - we now don't allow elements to exist if the schema doesn't validate, but wouldn't be too suprised if we end up back here at some point... + // will inevitably have some SNAFU somewhere. + // sorry future code user/maintainer if that is so. + return `Corrupt SpatialData.toString(): '${error}'` + } } toJSON() { - if (!this.parsed) return this; - return serializeZarrTree(this.parsed); + if (!this.rootStore.tree) return this; + return serializeZarrTree(this.rootStore.tree); } } diff --git a/packages/zarrextra/src/index.ts b/packages/zarrextra/src/index.ts index 589c530..03cc247 100644 --- a/packages/zarrextra/src/index.ts +++ b/packages/zarrextra/src/index.ts @@ -1,5 +1,5 @@ import * as zarr from 'zarrita'; -import type { ZarrTree, ConsolidatedStore, ZAttrsAny, IntermediateConsolidatedStore, ZarrV2Metadata, ZarrV3Metadata } from './types'; +import type { ZarrTree, ConsolidatedStore, ZAttrsAny, IntermediateConsolidatedStore, ZarrV2Metadata, ZarrV3Metadata, ZarrV3GroupNode, ZarrV3ArrayNode } from './types'; import { ATTRS_KEY, ZARRAY_KEY } from './types'; import { Err, Ok, type Result } from './result'; @@ -13,20 +13,20 @@ import { Err, Ok, type Result } from './result'; * relying on store.contents(). */ async function parseStoreContents(store: IntermediateConsolidatedStore): Promise { - // All metadata is normalized to v3 nested format - const metadata = store.zmetadata.metadata; + // Access metadata from consolidated_metadata.metadata + const metadata = store.zmetadata.consolidated_metadata?.metadata; if (!metadata || typeof metadata !== 'object') { return {}; } // Extract all paths from metadata and determine their types - const pathInfo = new Map(); + const pathInfo = new Map(); - for (const [path, pathMetadata] of Object.entries(metadata)) { - if (pathMetadata && typeof pathMetadata === 'object') { - const isArray = '.zarray' in pathMetadata; - pathInfo.set(path, { isArray, path }); + for (const [path, node] of Object.entries(metadata)) { + if (node && typeof node === 'object' && 'node_type' in node) { + const isArray = node.node_type === 'array'; + pathInfo.set(path, { isArray, path, node }); } } @@ -61,14 +61,27 @@ async function parseStoreContents(store: IntermediateConsolidatedStore): Promise const isArray = isLeaf && info.isArray; // Get attributes for this path (normalizedPath already has leading /) - const attrs = await getZattrs(normalizedPath as zarr.AbsolutePath, store); + const attrs = getZattrs(normalizedPath as zarr.AbsolutePath, store); if (isArray) { - // Leaf array node - const zarray = await getZattrs(normalizedPath as zarr.AbsolutePath, store, ".zarray"); + // Leaf array node - extract array metadata from the node + const arrayNode = info.node as ZarrV3ArrayNode; + const zarray: ZAttrsAny = { + shape: arrayNode.shape, + data_type: arrayNode.data_type, + chunk_grid: arrayNode.chunk_grid, + chunk_key_encoding: arrayNode.chunk_key_encoding, + fill_value: arrayNode.fill_value, + codecs: arrayNode.codecs, + dimension_names: arrayNode.dimension_names, + zarr_format: arrayNode.zarr_format, + node_type: arrayNode.node_type, + storage_transformers: arrayNode.storage_transformers, + }; + currentNode[part] = { [ATTRS_KEY]: attrs, - [ZARRAY_KEY]: zarray ?? ({} as ZAttrsAny), + [ZARRAY_KEY]: zarray, get: () => zarr.open(root.resolve(normalizedPath), { kind: 'array' }) }; } else { @@ -86,62 +99,124 @@ async function parseStoreContents(store: IntermediateConsolidatedStore): Promise /** * Parse zarr v3 consolidated metadata from zarr.json - * In zarr v3, consolidated metadata may have a different structure than v2. - * This function normalizes it to a format compatible with our getZattrs function. + * The actual zarr v3 structure has metadata nested under consolidated_metadata.metadata + * We normalize it to have a top-level metadata field for internal use. */ async function parseZarrJson(zarrJson: unknown): Promise> { if (!zarrJson || typeof zarrJson !== 'object') { return Err(new Error(`Invalid zarr.json: expected an object but got ${typeof zarrJson}`)); } - // zarr v3 zarr.json structure can vary, but typically has metadata nested by path const parsed = zarrJson as ZarrV3Metadata; - // If it already has the expected structure, return as-is - if (parsed.metadata && typeof parsed.metadata === 'object') { - return Ok(parsed); + // Validate the structure + if (!parsed.consolidated_metadata || typeof parsed.consolidated_metadata !== 'object') { + return Err(new Error('Invalid zarr.json: consolidated_metadata field is missing or not an object')); } - // Otherwise, try to normalize the structure - // This is a fallback for different zarr v3 implementations - const normalized: ZarrV3Metadata = { - metadata: parsed.metadata || {}, - ...parsed - }; - - if (!normalized.metadata || typeof normalized.metadata !== 'object') { - return Err(new Error('Invalid zarr.json: metadata field is missing or not an object')); + if (!parsed.consolidated_metadata.metadata || typeof parsed.consolidated_metadata.metadata !== 'object') { + return Err(new Error('Invalid zarr.json: consolidated_metadata.metadata field is missing or not an object')); } - return Ok(normalized); + // Return the parsed structure as-is (it matches our type definition) + return Ok(parsed); } /** - * Normalize zarr v2 flat metadata to nested structure like v3 - * Converts flat structure like { "path/.zattrs": {...} } - * to nested structure like { "path": { ".zattrs": {...} } } + * Normalize zarr v2 flat metadata to v3 structure + * Converts flat structure like { "path/.zattrs": {...}, "path/.zarray": {...} } + * to v3 structure with consolidated_metadata.metadata containing nodes */ function normalizeV2ToV3Metadata(v2Metadata: ZarrV2Metadata): ZarrV3Metadata { - const nested: ZarrV3Metadata['metadata'] = {}; + const metadata: Record = {}; if (!v2Metadata.metadata || typeof v2Metadata.metadata !== 'object') { - return { metadata: {} }; + return { + attributes: {}, + zarr_format: 3, + consolidated_metadata: { + kind: 'inline', + must_understand: false, + metadata: {} + }, + node_type: 'group' + }; } + // Group paths by their base path (without .zattrs, .zarray, .zgroup suffix) + const pathGroups = new Map(); + // Iterate through flat path keys in v2 metadata for (const [flatPath, value] of Object.entries(v2Metadata.metadata)) { // Match patterns like "path/.zattrs", "path/.zarray", "path/.zgroup" const match = flatPath.match(/^(.+?)\/(\.zattrs|\.zarray|\.zgroup)$/); if (match) { const [, path, metadataType] = match; - if (!nested[path]) { - nested[path] = {}; + if (!pathGroups.has(path)) { + pathGroups.set(path, {}); } - nested[path][metadataType as '.zattrs' | '.zarray' | '.zgroup'] = value; + const group = pathGroups.get(path); + if (group) { + if (metadataType === '.zattrs') { + group.zattrs = value; + } else if (metadataType === '.zarray') { + group.zarray = value; + } else if (metadataType === '.zgroup') { + group.zgroup = value; + } + } + } + } + + // Convert grouped paths to v3 node structure + for (const [path, group] of pathGroups.entries()) { + if (group.zarray) { + // Array node - use zarray metadata as the base + const zarray = group.zarray as Record; + metadata[path] = { + shape: (zarray.shape as number[]) || [], + data_type: (zarray.data_type as string) || 'float64', + chunk_grid: (zarray.chunk_grid as ZarrV3ArrayNode['chunk_grid']) || { + name: 'regular', + configuration: { chunk_shape: [] } + }, + chunk_key_encoding: (zarray.chunk_key_encoding as ZarrV3ArrayNode['chunk_key_encoding']) || { + name: 'default', + configuration: { separator: '/' } + }, + fill_value: (zarray.fill_value as number | string | boolean) || 0, + codecs: (zarray.codecs as ZarrV3ArrayNode['codecs']) || [], + attributes: (group.zattrs as Record) || {}, + dimension_names: (zarray.dimension_names as string[]) || [], + zarr_format: (zarray.zarr_format as number) || 3, + node_type: 'array', + storage_transformers: (zarray.storage_transformers as unknown[]) || [] + }; + } else { + // Group node + metadata[path] = { + attributes: (group.zattrs as Record) || {}, + zarr_format: 3, + consolidated_metadata: { + kind: 'inline', + must_understand: false, + metadata: {} + }, + node_type: 'group' + }; } } - return { metadata: nested }; + return { + attributes: {}, + zarr_format: 3, + consolidated_metadata: { + kind: 'inline', + must_understand: false, + metadata + }, + node_type: 'group' + }; } // we might not always use the FetchStore, this is for convenience & could change @@ -214,6 +289,8 @@ async function tryConsolidated(store: zarr.FetchStore): Promise> { // could `source` also be a File or something? try { + // why does this later end up thinking it should be able to do use HTTPMethd.PUT? + // seems inappropriate for a read-only store. const store = new zarr.FetchStore(source); const zarritaStore = await tryConsolidated(store); // Validate that we have metadata (we no longer check for contents() since we don't use it) @@ -234,18 +311,29 @@ export async function openExtraConsolidated(source: string): Promise | undefined> { +function getZattrs(path: zarr.AbsolutePath, store: IntermediateConsolidatedStore, k=".zattrs"): Record | undefined { const pathStr = path.slice(1); // Remove leading '/' - const pathMetadata = store.zmetadata.metadata?.[pathStr]; - if (!pathMetadata || typeof pathMetadata !== 'object') { + const metadata = store.zmetadata.consolidated_metadata?.metadata; + if (!metadata || typeof metadata !== 'object') { + return undefined; + } + + const node = metadata[pathStr]; + if (!node || typeof node !== 'object' || !('node_type' in node)) { return undefined; } - const attr = pathMetadata[k as '.zattrs' | '.zarray' | '.zgroup']; - if (!attr) return undefined; - return attr as Record; + // For backward compatibility, if k is ".zattrs", return the attributes field + // Otherwise, this function might be called with ".zarray" but in v3 we don't need that + // since array metadata is in the node itself + if (k === '.zattrs') { + return node.attributes; + } + + // For other keys, return undefined (legacy support) + return undefined; } /** diff --git a/packages/zarrextra/src/types.ts b/packages/zarrextra/src/types.ts index 6b51a50..e640bfb 100644 --- a/packages/zarrextra/src/types.ts +++ b/packages/zarrextra/src/types.ts @@ -50,14 +50,65 @@ export type ZarrV2Metadata = { metadata: Record; }; +/** + * Zarr v3 array node metadata + */ +export type ZarrV3ArrayNode = { + shape: number[]; + data_type: string; + chunk_grid: { + name: string; + configuration: { + chunk_shape: number[]; + }; + }; + chunk_key_encoding: { + name: string; + configuration: { + separator: string; + }; + }; + fill_value: number | string | boolean; + codecs: Array<{ + name: string; + configuration?: Record; + }>; + attributes: Record; + dimension_names: string[]; + zarr_format: number; + node_type: 'array'; + storage_transformers: unknown[]; +}; + +/** + * Zarr v3 group node metadata + */ +export type ZarrV3GroupNode = { + attributes: Record; + zarr_format: number; + consolidated_metadata: { + kind: string; + must_understand: boolean; + metadata: Record; + }; + node_type: 'group'; +}; + /** * Zarr v3 consolidated metadata structure (zarr.json) - * Structure may vary, but typically has nested metadata per path + * The actual structure has metadata nested under consolidated_metadata.metadata + * with path keys like "images/blobs_image", "labels/blobs_labels", etc. + * Each entry can be either a group node or an array node. */ export type ZarrV3Metadata = { - metadata?: Record; - // May have other fields depending on zarr v3 spec - [key: string]: unknown; + attributes: Record; + zarr_format: number; + consolidated_metadata: { + kind: string; + must_understand: boolean; + metadata: Record; + }; + node_type: 'group'; }; /** @@ -74,7 +125,8 @@ export type MetadataFormat = /** * A zarrita store with metadata appended as `zmetadata` - mostly for internal use and subject to revision. - * All metadata is normalized to v3 nested format internally, regardless of the original format (v2 or v3). + * For zarr v3, metadata is in the actual structure with consolidated_metadata.metadata. + * For zarr v2, we normalize it to match the v3 structure internally. * Uses `Store` (FetchStore) which already implements `Readable` - we work directly with metadata * and don't need `contents()` from `Listable`. */ From c4b56e8f37e5e05b1209c0b93d2fceb516c1ad96 Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Fri, 12 Dec 2025 22:40:36 +0000 Subject: [PATCH 05/50] changes missing from previous commit --- packages/core/src/types.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 5615593..ec1bca4 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -14,7 +14,7 @@ import type * as ad from 'anndata.js'; import type * as zarr from 'zarrita'; -import type { ZarrTree } from '@spatialdata/zarrextra'; +import type { ConsolidatedStore } from '@spatialdata/zarrextra'; /** * Element name constants and types @@ -87,8 +87,7 @@ export { Ok, Err, isOk, isErr, unwrap, unwrapOr } from '@spatialdata/zarrextra'; */ export type SDataProps = { url: StoreLocation; - parsed?: ZarrTree; onBadFiles?: BadFileHandler; selection?: ElementName[]; - rootStore: zarr.Listable; + rootStore: ConsolidatedStore; } \ No newline at end of file From f4ee85bc89f5fc889a38de5f3b17549628882f86 Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Fri, 12 Dec 2025 22:45:36 +0000 Subject: [PATCH 06/50] add config for generating tests with python, against spatialdata 0.5 & 0.6, also proxy server script, workflow, vscode settings noise --- .cursor/settings.json | 15 ++ .github/workflows/test.yml | 50 +++++ .gitignore | 11 +- .vscode/settings.json | 15 ++ README.md | 158 +++++++++++++++ package.json | 8 + python/.python-version | 2 + python/scripts/generate_fixtures.py | 100 +++++++++ python/v0.5.0/generate_fixtures.py | 176 ++++++++++++++++ python/v0.5.0/pyproject.toml | 9 + python/v0.6.1/generate_fixtures.py | 130 ++++++++++++ python/v0.6.1/pyproject.toml | 9 + scripts/cors-proxy.js | 193 ++++++++++++++++++ scripts/test-server.js | 163 +++++++++++++++ tests/integration/fixtures.test.ts | 171 ++++++++++++++++ tests/unit/schemas.test.ts | 303 ++++++++++++++++++++++++++++ vitest.config.ts | 23 +++ 17 files changed, 1535 insertions(+), 1 deletion(-) create mode 100644 .cursor/settings.json create mode 100644 .github/workflows/test.yml create mode 100644 .vscode/settings.json create mode 100644 python/.python-version create mode 100644 python/scripts/generate_fixtures.py create mode 100755 python/v0.5.0/generate_fixtures.py create mode 100644 python/v0.5.0/pyproject.toml create mode 100755 python/v0.6.1/generate_fixtures.py create mode 100644 python/v0.6.1/pyproject.toml create mode 100755 scripts/cors-proxy.js create mode 100755 scripts/test-server.js create mode 100644 tests/integration/fixtures.test.ts create mode 100644 tests/unit/schemas.test.ts create mode 100644 vitest.config.ts diff --git a/.cursor/settings.json b/.cursor/settings.json new file mode 100644 index 0000000..7ff7a3d --- /dev/null +++ b/.cursor/settings.json @@ -0,0 +1,15 @@ +{ + // Note: Python environments are version-specific. Choose one: + // For spatialdata 0.5.0: python/v0.5.0/.venv/bin/python3 + // For spatialdata 0.6.1: python/v0.6.1/.venv/bin/python3 + "python.defaultInterpreterPath": "${workspaceFolder}/python/v0.6.1/.venv/bin/python3", + "python.terminal.activateEnvironment": true, + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter", + "editor.formatOnSave": true + }, + "python.analysis.extraPaths": [ + "${workspaceFolder}/python" + ] +} + diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..2c659cc --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,50 @@ +name: Test + +on: + push: + branches: + - main + pull_request: + branches: + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - uses: astral-sh/setup-uv@v4 + with: + version: "latest" + + - name: Install pnpm + run: npm install -g pnpm + + - name: Install dependencies + run: pnpm install + + - name: Cache uv environments + uses: actions/cache@v4 + with: + path: ~/.cache/uv + key: ${{ runner.os }}-uv-${{ hashFiles('python/v0.5.0/pyproject.toml', 'python/v0.6.1/pyproject.toml') }} + + # Note: Version-specific environments are set up automatically by the fixture generation script + # No need to manually sync here unless you want to pre-warm the cache + + - name: Run unit tests + run: pnpm test:unit + + - name: Run integration tests + run: pnpm test:integration + diff --git a/.gitignore b/.gitignore index 9d64d2a..050601d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,13 +9,22 @@ build/ # Testing coverage/ +test-fixtures/ + +# Python virtual environments (version-specific, completely separate) +python/v0.5.0/.venv/ +python/v0.6.1/.venv/ +python/v0.5.0/uv.lock +python/v0.6.1/uv.lock # Environment .env .env.local # IDE -.vscode/ +# Note: .vscode/settings.json and .cursor/settings.json are committed for Python interpreter setup +.vscode/launch.json +.vscode/tasks.json .idea/ *.swp *.swo diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7ff7a3d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,15 @@ +{ + // Note: Python environments are version-specific. Choose one: + // For spatialdata 0.5.0: python/v0.5.0/.venv/bin/python3 + // For spatialdata 0.6.1: python/v0.6.1/.venv/bin/python3 + "python.defaultInterpreterPath": "${workspaceFolder}/python/v0.6.1/.venv/bin/python3", + "python.terminal.activateEnvironment": true, + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter", + "editor.formatOnSave": true + }, + "python.analysis.extraPaths": [ + "${workspaceFolder}/python" + ] +} + diff --git a/README.md b/README.md index 0709b58..efdff78 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ This monorepo contains: - Node.js >= 20 - pnpm >= 10 +- Python >= 3.12 (for generating test fixtures) +- [uv](https://github.com/astral-sh/uv) (Python package manager) ### Installation @@ -48,6 +50,162 @@ pnpm format pnpm docs:dev ``` +## Testing + +### Prerequisites for Testing + +To run the full test suite, you'll need: + +- Python >= 3.12 +- [uv](https://github.com/astral-sh/uv) - Python package manager + +#### Setting Up the Python Environment (not required for JS stuff, mostly generating test fixtures) + +The Python environment is managed by `uv` and defined in `python/pyproject.toml`. To set it up: + +**Installing uv:** +```bash +# Install uv (Unix/macOS) +curl -LsSf https://astral.sh/uv/install.sh | sh + +# Or using pip/pipx +pip install uv +# or +pipx install uv +``` + + +**Setting up the environments:** + +The project uses completely separate Python environments for each spatialdata version. Each version has its own directory with its own `pyproject.toml` and virtual environment: + +```bash +# Set up environment for spatialdata 0.5.0 +uv sync --directory python/v0.5.0 + +# Set up environment for spatialdata 0.6.1 +uv sync --directory python/v0.6.1 + +# Or set up both at once (the fixture generation script will do this automatically) +``` + +Each environment: +- Is completely independent with its own virtual environment at `python/v{version}/.venv/` +- Has its own `pyproject.toml` that pins a specific spatialdata version +- Is isolated from other versions (no shared base environment) + +**Note:** The fixture generation script automatically sets up these environments when needed. You only need to manually sync if you want to activate an environment directly or use it in your editor. + +#### Editor Setup + +The project includes editor configuration files (`.vscode/settings.json` and `.cursor/settings.json`) that configure the Python interpreter to use one of the version-specific environments (defaults to v0.6.1). + +**VS Code / Cursor:** +- The Python interpreter is automatically configured when you open the workspace +- To switch versions: `Cmd/Ctrl+Shift+P` → "Python: Select Interpreter" → Choose: + - `./python/v0.5.0/.venv/bin/python3` for spatialdata 0.5.0 + - `./python/v0.6.1/.venv/bin/python3` for spatialdata 0.6.1 + +**Other editors:** +- Choose the appropriate virtual environment: + - `python/v0.5.0/.venv/bin/python3` for spatialdata 0.5.0 + - `python/v0.6.1/.venv/bin/python3` for spatialdata 0.6.1 + + +### Running Tests + +```bash +# Run all tests (unit + integration) +pnpm test:all + +# Run only unit tests (fast, no fixtures needed) +pnpm test:unit + +# Run only integration tests (requires fixtures) +pnpm test:integration + +# Run tests from individual packages +pnpm test +``` + +### Generating Test Fixtures + +Test fixtures are generated on-demand using the Python `spatialdata` library. Each version uses a separate Python environment with the specific spatialdata version pinned to ensure accurate fixture generation. + +Fixtures are stored in `test-fixtures/` (excluded from git). + +```bash +# Generate fixtures for both spatialdata versions (0.5.0 and 0.6.1) +# This will automatically set up the version-specific environments if needed +pnpm test:fixtures:generate + +# Generate fixtures for a specific version +pnpm test:fixtures:generate:0.5.0 +pnpm test:fixtures:generate:0.6.1 +``` + +**How it works:** +- The script uses separate environments: `python/v0.5.0/` and `python/v0.6.1/` +- Each environment has its own `pyproject.toml` with the spatialdata version pinned +- The script automatically runs `uv sync` for each environment before generating fixtures +- This ensures fixtures are generated with the exact spatialdata version being tested + +**Note:** Integration tests will automatically generate fixtures if they're missing, but you can pre-generate them for faster test runs. + +### Test Servers + +#### Test Fixture Server + +The test fixture server serves generated fixtures over HTTP for testing with `FetchStore`: + +```bash +# Start the test fixture server (runs on http://localhost:8080) +pnpm test:server +``` + +Once running, fixtures are accessible at: +- `http://localhost:8080/test-fixtures/v0.5.0/blobs.zarr` +- `http://localhost:8080/test-fixtures/v0.6.1/blobs.zarr` + +The server provides directory listings and serves all zarr metadata files with appropriate CORS headers. + +#### CORS Proxy Server + +The CORS proxy server allows accessing spatialdata stores that don't have CORS headers enabled. This is useful for local development when testing against remote stores. + +```bash +# Start the CORS proxy server (runs on http://localhost:8081) +pnpm test:proxy +``` + +**Usage:** + +Proxy a remote URL by appending it as a query parameter: +``` +http://localhost:8081/?url=https://example.com/data.zarr/.zattrs +``` + +Or use it as a path (for convenience): +``` +http://localhost:8081/https://example.com/data.zarr/.zattrs +``` + +**Example:** + +If you have a spatialdata store at `https://example.com/mydata.zarr` that doesn't have CORS headers, you can access it through the proxy: + +```typescript +import { readZarr } from '@spatialdata/core'; + +// Instead of: +// const sdata = await readZarr('https://example.com/mydata.zarr'); + +// Use the proxy: +const sdata = await readZarr('http://localhost:8081/?url=https://example.com/mydata.zarr'); +``` + +**⚠️ Warning:** The CORS proxy is for local development only. It has no security restrictions and should never be exposed to the internet. + ## 📝 License diff --git a/package.json b/package.json index 5f49638..28cfc59 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,14 @@ "scripts": { "build": "pnpm -r --filter='!docs' run build", "test": "pnpm -r --filter='!docs' run test", + "test:unit": "vitest run tests/unit", + "test:integration": "vitest run tests/integration", + "test:all": "pnpm test:unit && pnpm test:integration", + "test:fixtures:generate": "uv run python/scripts/generate_fixtures.py", + "test:fixtures:generate:0.5.0": "uv run python/scripts/generate_fixtures.py --version 0.5.0", + "test:fixtures:generate:0.6.1": "uv run python/scripts/generate_fixtures.py --version 0.6.1", + "test:server": "node scripts/test-server.js", + "test:proxy": "node scripts/cors-proxy.js", "lint": "biome check .", "format": "biome format --write .", "format:check": "biome format .", diff --git a/python/.python-version b/python/.python-version new file mode 100644 index 0000000..e0aad9e --- /dev/null +++ b/python/.python-version @@ -0,0 +1,2 @@ +3.12 + diff --git a/python/scripts/generate_fixtures.py b/python/scripts/generate_fixtures.py new file mode 100644 index 0000000..bda6c84 --- /dev/null +++ b/python/scripts/generate_fixtures.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +""" +Wrapper script to generate test fixtures for all spatialdata versions. + +This script coordinates running the version-specific fixture generation scripts +located in python/v0.5.0/ and python/v0.6.1/. +""" + +import argparse +import subprocess +import sys +from pathlib import Path + + +def main(): + parser = argparse.ArgumentParser( + description="Generate test fixtures for SpatialData.ts" + ) + parser.add_argument( + "--version", + type=str, + choices=["0.5.0", "0.6.1"], + default=None, + help="SpatialData version to generate fixtures for (default: both)", + ) + parser.add_argument( + "--output-dir", + type=str, + default="test-fixtures", + help="Output directory for fixtures (default: test-fixtures)", + ) + + args = parser.parse_args() + + # Get project root + script_path = Path(__file__) + project_root = script_path.parent.parent.parent + output_dir = project_root / args.output_dir + + versions = [args.version] if args.version else ["0.5.0", "0.6.1"] + + success = True + for version in versions: + env_dir = project_root / "python" / f"v{version}" + version_script = env_dir / "generate_fixtures.py" + + if not version_script.exists(): + print(f"Error: Script not found at {version_script}") + print(f" Make sure the environment directory exists: {env_dir}") + success = False + continue + + # Ensure the environment is set up + print(f"\n{'='*60}") + print(f"Setting up environment for spatialdata {version}...") + print(f"{'='*60}") + sync_result = subprocess.run( + ["uv", "sync", "--directory", str(env_dir)], + cwd=project_root, + capture_output=True, + text=True, + ) + + if sync_result.returncode != 0: + print(f"Error setting up environment for version {version}:") + print(sync_result.stderr) + success = False + continue + + # Run the version-specific script in its environment + print(f"\n{'='*60}") + print(f"Generating fixtures for spatialdata {version}...") + print(f"{'='*60}") + result = subprocess.run( + [ + "uv", "run", + "--directory", str(env_dir), + str(version_script), + "--output-dir", str(output_dir), + ], + cwd=project_root, + ) + + if result.returncode != 0: + print(f"Failed to generate fixtures for version {version}") + success = False + + if success: + print("\n" + "="*60) + print("✓ All fixtures generated successfully!") + print("="*60) + else: + print("\n" + "="*60) + print("✗ Some fixtures failed to generate") + print("="*60) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/python/v0.5.0/generate_fixtures.py b/python/v0.5.0/generate_fixtures.py new file mode 100755 index 0000000..0036fe5 --- /dev/null +++ b/python/v0.5.0/generate_fixtures.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +""" +Generate test fixtures using spatialdata library (version 0.5.0). + +This script generates spatialdata zarr stores for testing with spatialdata 0.5.0. +It runs in the python/v0.5.0/ environment which has spatialdata==0.5.0 pinned. +""" + +import sys +from pathlib import Path + +# Add scripts directory to path if we need shared utilities +script_dir = Path(__file__).parent +sys.path.insert(0, str(script_dir.parent / "scripts")) + +from spatialdata.datasets import blobs +import spatialdata as sd + + +def generate_fixtures(output_dir: Path): + """Generate test fixtures for spatialdata version 0.5.0.""" + version = "0.5.0" + print(f"Generating fixtures for spatialdata version {version}...") + + # Verify we're using the correct version + actual_version = sd.__version__ + if actual_version != version: + print(f"⚠️ Warning: Expected version {version} but got {actual_version}") + print(f" This may indicate the wrong environment is active.") + + # Create output directory + version_dir = output_dir / f"v{version}" + version_dir.mkdir(parents=True, exist_ok=True) + + # Generate a simple spatialdata object using blobs dataset + print("Creating spatialdata object with blobs dataset...") + sdata = blobs() + + # Workaround for spatialdata 0.5.0 bug: Remove points that have Identity transformations + # which can't be serialized to JSON. This is a known issue in 0.5.0. + if hasattr(sdata, "points") and sdata.points: + print("Removing points data (workaround for spatialdata 0.5.0 JSON serialization bug)...") + # Clear points - handle both dict and list cases + if isinstance(sdata.points, dict): + sdata.points.clear() + elif isinstance(sdata.points, list): + sdata.points.clear() + else: + # Try to delete the attribute + try: + delattr(sdata, "points") + except: + # If that fails, set to empty dict + sdata.points = {} + + # Save to zarr store + store_path = version_dir / "blobs.zarr" + + # Remove existing store if it exists (to allow regenerating fixtures) + import shutil + import os + if os.path.exists(store_path) or os.path.isdir(store_path): + print(f"Removing existing fixture at {store_path}...") + try: + if os.path.isdir(store_path): + shutil.rmtree(store_path) + elif os.path.isfile(store_path): + os.remove(store_path) + except Exception as e: + print(f"Warning: Could not remove existing fixture: {e}") + + # Double-check it's gone + if os.path.exists(store_path): + print(f"Warning: Store path still exists after removal attempt: {store_path}") + # Force remove + try: + shutil.rmtree(store_path, ignore_errors=True) + except: + pass + + print(f"Saving to {store_path}...") + + # Use spatialdata's write method + # Note: spatialdata 0.5.0 has a known issue with JSON serialization of Identity + # transformations when writing points. We'll catch this and clean up. + try: + # Try with overwrite first (for newer versions) + try: + sdata.write(store_path, overwrite=True) + except TypeError: + # If overwrite parameter doesn't exist, try without it + sdata.write(store_path) + except Exception as e: + # Clean up any partially written store + if os.path.exists(store_path): + try: + if os.path.isdir(store_path): + shutil.rmtree(store_path) + else: + os.remove(store_path) + except: + pass + + error_msg = str(e) + error_type = type(e).__name__ + + # Check for JSON serialization issues (known bug in spatialdata 0.5.0) + if "JSON" in error_msg or "serializable" in error_msg.lower() or "Identity" in error_msg: + print(f"\n⚠️ Error: {error_type}: {error_msg}") + print("\nThis appears to be a known issue with spatialdata 0.5.0 where") + print("Identity transformations cannot be serialized to JSON when writing points.") + raise RuntimeError( + f"Failed to generate fixtures due to spatialdata 0.5.0 bug: {error_msg}" + ) from e + else: + # Re-raise other errors + raise + + print(f"✓ Generated fixture at {store_path}") + + # Print some metadata about what was generated + print("\nGenerated elements:") + for element_type in ["images", "labels", "points", "shapes", "tables"]: + elements = getattr(sdata, element_type, None) + if elements: + # Handle both dict and list cases + if isinstance(elements, dict): + print(f" - {element_type}: {len(elements)} element(s)") + for name in elements.keys(): + print(f" * {name}") + elif isinstance(elements, list): + print(f" - {element_type}: {len(elements)} element(s)") + for i, elem in enumerate(elements): + print(f" * element_{i}") + else: + print(f" - {element_type}: present") + + if hasattr(sdata, "coordinate_systems"): + if isinstance(sdata.coordinate_systems, dict): + print(f"\nCoordinate systems: {list(sdata.coordinate_systems.keys())}") + elif isinstance(sdata.coordinate_systems, list): + print(f"\nCoordinate systems: {sdata.coordinate_systems}") + else: + print(f"\nCoordinate systems: {sdata.coordinate_systems}") + + print(f"\nUsing spatialdata version {actual_version}") + + return store_path + + +def main(): + import argparse + + parser = argparse.ArgumentParser( + description="Generate test fixtures for SpatialData.ts using spatialdata 0.5.0" + ) + parser.add_argument( + "--output-dir", + type=str, + default="test-fixtures", + help="Output directory for fixtures (default: test-fixtures)", + ) + + args = parser.parse_args() + + # Get project root (parent of python/ directory) + project_root = Path(__file__).parent.parent.parent + output_dir = project_root / args.output_dir + + generate_fixtures(output_dir) + print("\n✓ Fixtures generated successfully!") + + +if __name__ == "__main__": + main() + diff --git a/python/v0.5.0/pyproject.toml b/python/v0.5.0/pyproject.toml new file mode 100644 index 0000000..2bc1fc1 --- /dev/null +++ b/python/v0.5.0/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "spatialdata-test-fixtures-v0.5.0" +version = "0.1.0" +description = "Test fixture generation for SpatialData.ts using spatialdata 0.5.0" +requires-python = ">=3.12" +dependencies = [ + "spatialdata==0.5.0", +] + diff --git a/python/v0.6.1/generate_fixtures.py b/python/v0.6.1/generate_fixtures.py new file mode 100755 index 0000000..09ca175 --- /dev/null +++ b/python/v0.6.1/generate_fixtures.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +""" +Generate test fixtures using spatialdata library (version 0.6.1). + +This script generates spatialdata zarr stores for testing with spatialdata 0.6.1. +It runs in the python/v0.6.1/ environment which has spatialdata==0.6.1 pinned. +""" + +import sys +from pathlib import Path + +# Add scripts directory to path if we need shared utilities +script_dir = Path(__file__).parent +sys.path.insert(0, str(script_dir.parent / "scripts")) + +from spatialdata.datasets import blobs +import spatialdata as sd + + +def generate_fixtures(output_dir: Path): + """Generate test fixtures for spatialdata version 0.6.1.""" + version = "0.6.1" + print(f"Generating fixtures for spatialdata version {version}...") + + # Verify we're using the correct version + actual_version = sd.__version__ + if actual_version != version: + print(f"⚠️ Warning: Expected version {version} but got {actual_version}") + print(f" This may indicate the wrong environment is active.") + + # Create output directory + version_dir = output_dir / f"v{version}" + version_dir.mkdir(parents=True, exist_ok=True) + + # Generate a simple spatialdata object using blobs dataset + print("Creating spatialdata object with blobs dataset...") + sdata = blobs() + + # Save to zarr store + store_path = version_dir / "blobs.zarr" + + # Remove existing store if it exists (to allow regenerating fixtures) + import shutil + if store_path.exists(): + print(f"Removing existing fixture at {store_path}...") + shutil.rmtree(store_path) + + print(f"Saving to {store_path}...") + + # Use spatialdata's write method + # The API may vary by version, so we try different methods + try: + # Try the standard write method (most common) + sdata.write(store_path, overwrite=True) + except TypeError: + # If overwrite parameter doesn't exist, try without it + # (older versions might not support it) + sdata.write(store_path) + except (AttributeError, ValueError) as e: + # If write doesn't exist or other error, try alternatives + if "already exists" in str(e).lower(): + # Shouldn't happen since we removed it, but handle anyway + shutil.rmtree(store_path) + sdata.write(store_path) + else: + # Fallback: try write_zarr if write doesn't exist + try: + sdata.write_zarr(store_path) + except AttributeError: + # Another fallback: use sd.io.write_zarr + sd.io.write_zarr(sdata, store_path) + + print(f"✓ Generated fixture at {store_path}") + + # Print some metadata about what was generated + print("\nGenerated elements:") + for element_type in ["images", "labels", "points", "shapes", "tables"]: + elements = getattr(sdata, element_type, None) + if elements: + # Handle both dict and list cases + if isinstance(elements, dict): + print(f" - {element_type}: {len(elements)} element(s)") + for name in elements.keys(): + print(f" * {name}") + elif isinstance(elements, list): + print(f" - {element_type}: {len(elements)} element(s)") + for i, elem in enumerate(elements): + print(f" * element_{i}") + else: + print(f" - {element_type}: present") + + if hasattr(sdata, "coordinate_systems"): + if isinstance(sdata.coordinate_systems, dict): + print(f"\nCoordinate systems: {list(sdata.coordinate_systems.keys())}") + elif isinstance(sdata.coordinate_systems, list): + print(f"\nCoordinate systems: {sdata.coordinate_systems}") + else: + print(f"\nCoordinate systems: {sdata.coordinate_systems}") + + print(f"\nUsing spatialdata version {actual_version}") + + return store_path + + +def main(): + import argparse + + parser = argparse.ArgumentParser( + description="Generate test fixtures for SpatialData.ts using spatialdata 0.6.1" + ) + parser.add_argument( + "--output-dir", + type=str, + default="test-fixtures", + help="Output directory for fixtures (default: test-fixtures)", + ) + + args = parser.parse_args() + + # Get project root (parent of python/ directory) + project_root = Path(__file__).parent.parent.parent + output_dir = project_root / args.output_dir + + generate_fixtures(output_dir) + print("\n✓ Fixtures generated successfully!") + + +if __name__ == "__main__": + main() + diff --git a/python/v0.6.1/pyproject.toml b/python/v0.6.1/pyproject.toml new file mode 100644 index 0000000..661bc55 --- /dev/null +++ b/python/v0.6.1/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "spatialdata-test-fixtures-v0.6.1" +version = "0.1.0" +description = "Test fixture generation for SpatialData.ts using spatialdata 0.6.1" +requires-python = ">=3.12" +dependencies = [ + "spatialdata==0.6.1", +] + diff --git a/scripts/cors-proxy.js b/scripts/cors-proxy.js new file mode 100755 index 0000000..d688946 --- /dev/null +++ b/scripts/cors-proxy.js @@ -0,0 +1,193 @@ +#!/usr/bin/env node +/** + * CORS proxy server for local development. + * + * Proxies requests to any URL and adds CORS headers to responses. + * This allows accessing spatialdata stores that don't have CORS headers + * from browser-based applications. + * + * WARNING: This is for local development only. No security features are included. + */ + +import { createServer } from 'node:http'; +import { URL } from 'node:url'; + +const PORT = process.env.PORT || 8081; + +/** + * Add CORS headers to a response + */ +function addCorsHeaders(res) { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS, POST, PUT, DELETE'); + res.setHeader('Access-Control-Allow-Headers', '*'); + res.setHeader('Access-Control-Expose-Headers', '*'); + res.setHeader('Access-Control-Max-Age', '86400'); +} + +/** + * Handle OPTIONS preflight requests + */ +function handleOptions(req, res) { + addCorsHeaders(res); + res.writeHead(200); + res.end(); +} + +/** + * Proxy a request to the target URL + */ +async function proxyRequest(req, res, targetUrl) { + try { + const url = new URL(targetUrl); + + // Build fetch options + const fetchOptions = { + method: req.method, + headers: { ...req.headers }, + }; + + // Remove host header (will be set by fetch) + delete fetchOptions.headers.host; + delete fetchOptions.headers['content-length']; + + // Forward request body for POST/PUT + let body = null; + if (req.method === 'POST' || req.method === 'PUT') { + const chunks = []; + for await (const chunk of req) { + chunks.push(chunk); + } + body = Buffer.concat(chunks); + if (body.length > 0) { + fetchOptions.body = body; + } + } + + // Make the request + const response = await fetch(targetUrl, fetchOptions); + + // Add CORS headers + addCorsHeaders(res); + + // Copy response headers (except CORS-related ones which we set) + const headersToSkip = new Set([ + 'access-control-allow-origin', + 'access-control-allow-methods', + 'access-control-allow-headers', + 'access-control-expose-headers', + 'access-control-max-age', + ]); + + response.headers.forEach((value, key) => { + if (!headersToSkip.has(key.toLowerCase())) { + res.setHeader(key, value); + } + }); + + // Set status code + res.writeHead(response.status, res.getHeaders()); + + // Stream response body + if (response.body) { + const reader = response.body.getReader(); + const pump = async () => { + try { + while (true) { + const { done, value } = await reader.read(); + if (done) { + res.end(); + break; + } + res.write(value); + } + } catch (error) { + console.error('Error streaming response:', error); + res.end(); + } + }; + pump(); + } else { + res.end(); + } + } catch (error) { + console.error('Error proxying request:', error); + addCorsHeaders(res); + res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.end(`Proxy Error: ${error.message}`); + } +} + +/** + * Handle HTTP request + */ +async function handleRequest(req, res) { + // Handle OPTIONS preflight + if (req.method === 'OPTIONS') { + handleOptions(req, res); + return; + } + + try { + // Extract target URL from query parameter or path + const url = new URL(req.url, `http://${req.headers.host}`); + const targetUrl = url.searchParams.get('url') || url.pathname.slice(1); + + if (!targetUrl) { + addCorsHeaders(res); + res.writeHead(400, { 'Content-Type': 'text/plain' }); + res.end('Usage: /?url= or /'); + return; + } + + // Validate URL + let target; + try { + target = new URL(targetUrl); + } catch (error) { + // If not a full URL, try to construct one + // For relative paths, we can't proxy them + addCorsHeaders(res); + res.writeHead(400, { 'Content-Type': 'text/plain' }); + res.end('Invalid URL. Please provide a full URL (e.g., https://example.com/data.zarr)'); + return; + } + + // Only allow http/https protocols + if (target.protocol !== 'http:' && target.protocol !== 'https:') { + addCorsHeaders(res); + res.writeHead(400, { 'Content-Type': 'text/plain' }); + res.end('Only http and https protocols are supported'); + return; + } + + // Proxy the request + await proxyRequest(req, res, targetUrl); + } catch (error) { + console.error('Error handling request:', error); + addCorsHeaders(res); + res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.end('Internal Server Error'); + } +} + +// Create and start server +const server = createServer(handleRequest); + +server.listen(PORT, () => { + console.log(`CORS proxy server running at http://localhost:${PORT}`); + console.log(`\nUsage:`); + console.log(` GET http://localhost:${PORT}/?url=`); + console.log(` Example: http://localhost:${PORT}/?url=https://example.com/data.zarr/.zattrs`); + console.log(`\nPress Ctrl+C to stop\n`); +}); + +// Handle graceful shutdown +process.on('SIGINT', () => { + console.log('\nShutting down proxy server...'); + server.close(() => { + console.log('Proxy server stopped'); + process.exit(0); + }); +}); + diff --git a/scripts/test-server.js b/scripts/test-server.js new file mode 100755 index 0000000..040dc65 --- /dev/null +++ b/scripts/test-server.js @@ -0,0 +1,163 @@ +#!/usr/bin/env node +/** + * Simple static file server for serving test fixtures. + * + * Serves the test-fixtures directory so that fixtures can be accessed + * via HTTP URLs for testing with FetchStore. + */ + +import { createServer } from 'node:http'; +import { readFile, stat, readdir } from 'node:fs/promises'; +import { join, extname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { dirname } from 'node:path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const projectRoot = resolve(__dirname, '..'); +const fixturesDir = join(projectRoot, 'test-fixtures'); + +const PORT = process.env.PORT || 8080; + +/** + * Get MIME type for a file based on extension + */ +function getMimeType(filePath) { + const ext = extname(filePath).toLowerCase(); + const mimeTypes = { + '.json': 'application/json', + '.zarr': 'application/octet-stream', + '.zattrs': 'application/json', + '.zarray': 'application/json', + '.zgroup': 'application/json', + '.zmetadata': 'application/json', + '.html': 'text/html', + '.txt': 'text/plain', + }; + return mimeTypes[ext] || 'application/octet-stream'; +} + +/** + * Generate HTML directory listing + */ +function generateDirectoryListing(files, currentPath, basePath) { + const items = files + .map((file) => { + const isDir = file.isDirectory; + const href = `${currentPath}/${file.name}${isDir ? '/' : ''}`; + const icon = isDir ? '📁' : '📄'; + return `
  • ${icon} ${file.name}
  • `; + }) + .join('\n'); + + return ` + + + Test Fixtures - ${currentPath} + + + +

    Test Fixtures

    +

    Path: ${currentPath}

    +
      + ${items} +
    + +`; +} + +/** + * Handle HTTP request + */ +async function handleRequest(req, res) { + try { + // Remove query string and normalize path + const urlPath = new URL(req.url, `http://${req.headers.host}`).pathname; + + // Remove leading /test-fixtures if present (for cleaner URLs) + let filePath = urlPath.startsWith('/test-fixtures') + ? urlPath.slice('/test-fixtures'.length) + : urlPath; + + // Remove leading slash + filePath = filePath.startsWith('/') ? filePath.slice(1) : filePath; + + // Resolve to fixtures directory + const fullPath = join(fixturesDir, filePath); + + // Security: ensure path is within fixtures directory + if (!fullPath.startsWith(fixturesDir)) { + res.writeHead(403, { 'Content-Type': 'text/plain' }); + res.end('Forbidden'); + return; + } + + // Check if path exists + const stats = await stat(fullPath).catch(() => null); + + if (!stats) { + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('Not Found'); + return; + } + + // Handle directory listing + if (stats.isDirectory()) { + const files = await readdir(fullPath, { withFileTypes: true }); + const fileList = files.map((file) => ({ + name: file.name, + isDirectory: file.isDirectory(), + })); + + const html = generateDirectoryListing(fileList, urlPath, '/test-fixtures'); + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(html); + return; + } + + // Serve file + const content = await readFile(fullPath); + const mimeType = getMimeType(fullPath); + + // Add CORS headers for cross-origin requests + res.writeHead(200, { + 'Content-Type': mimeType, + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS', + 'Access-Control-Allow-Headers': 'Range', + 'Content-Length': content.length, + }); + + res.end(content); + } catch (error) { + console.error('Error handling request:', error); + res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.end('Internal Server Error'); + } +} + +// Create and start server +const server = createServer(handleRequest); + +server.listen(PORT, () => { + console.log(`Test fixture server running at http://localhost:${PORT}`); + console.log(`Serving fixtures from: ${fixturesDir}`); + console.log(`\nAccess fixtures at: http://localhost:${PORT}/test-fixtures/`); + console.log(`Press Ctrl+C to stop\n`); +}); + +// Handle graceful shutdown +process.on('SIGINT', () => { + console.log('\nShutting down server...'); + server.close(() => { + console.log('Server stopped'); + process.exit(0); + }); +}); + diff --git a/tests/integration/fixtures.test.ts b/tests/integration/fixtures.test.ts new file mode 100644 index 0000000..06cf5a4 --- /dev/null +++ b/tests/integration/fixtures.test.ts @@ -0,0 +1,171 @@ +import { describe, expect, it, beforeAll } from 'vitest'; +import { readZarr } from '../../packages/core/src/store/index.js'; +import { execSync } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { dirname } from 'node:path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const projectRoot = join(__dirname, '../..'); + +/** + * Ensure fixtures are generated for a specific version + */ +function ensureFixtures(version: string): string { + const fixturePath = join(projectRoot, 'test-fixtures', `v${version}`, 'blobs.zarr'); + + if (!existsSync(fixturePath)) { + console.log(`Fixtures not found for version ${version}, generating...`); + try { + execSync(`uv run python/scripts/generate_fixtures.py --version ${version}`, { + cwd: projectRoot, + stdio: 'inherit', + }); + } catch (error) { + throw new Error( + `Failed to generate fixtures for version ${version}. ` + + `Make sure uv is installed and spatialdata is available. Error: ${error}` + ); + } + } + + return fixturePath; +} + +/** + * Get the URL for a fixture (for use with readZarr) + * In a real scenario, this would be served by the test server + * For now, we'll use file:// URLs if the environment supports it, + * or we'll need to start a local server + */ +function getFixtureUrl(fixturePath: string): string { + // For now, we'll use a file:// URL + // Note: This may not work with FetchStore, which expects HTTP URLs + // In a real scenario, we'd start the test server and use http://localhost:8080/... + // For now, we'll test with a local path that might work with future local file support + return `file://${fixturePath}`; +} + +// Test matrix for different spatialdata versions +const versions = ['0.5.0', '0.6.1'] as const; + +describe.each(versions)('Integration Tests - spatialdata v%s', (version) => { + let fixturePath: string; + let fixtureUrl: string; + + beforeAll(() => { + fixturePath = ensureFixtures(version); + // For now, we'll need to use a workaround since FetchStore expects HTTP URLs + // We'll use a local server URL pattern that the test server would serve + // In practice, tests should start the test server first + fixtureUrl = `http://localhost:8080/test-fixtures/v${version}/blobs.zarr`; + }); + + it('should load spatialdata store', async () => { + // Note: This test requires the test server to be running + // In CI, we'd start it as part of the test setup + // For now, we'll skip if the server isn't available + try { + const sdata = await readZarr(fixtureUrl); + expect(sdata).toBeDefined(); + expect(sdata.url).toBe(fixtureUrl); + } catch (error) { + // If FetchStore doesn't support file:// or server isn't running, + // we'll skip this test with a helpful message + if (error instanceof Error && error.message.includes('fetch')) { + console.warn( + `Skipping integration test - test server not running. ` + + `Start it with: pnpm test:server` + ); + return; + } + throw error; + } + }, 30000); // 30 second timeout for fixture generation + + it('should parse elements from store', async () => { + try { + const sdata = await readZarr(fixtureUrl); + + // Check that we can access parsed structure + expect(sdata.parsed).toBeDefined(); + + // The blobs dataset should have at least images + // We'll check what elements are available + const hasImages = sdata.images !== undefined && Object.keys(sdata.images).length > 0; + const hasPoints = sdata.points !== undefined && Object.keys(sdata.points).length > 0; + const hasShapes = sdata.shapes !== undefined && Object.keys(sdata.shapes).length > 0; + const hasLabels = sdata.labels !== undefined && Object.keys(sdata.labels).length > 0; + const hasTables = sdata.tables !== undefined && Object.keys(sdata.tables).length > 0; + + // At least one element type should be present + expect(hasImages || hasPoints || hasShapes || hasLabels || hasTables).toBe(true); + } catch (error) { + if (error instanceof Error && error.message.includes('fetch')) { + console.warn('Skipping test - test server not running'); + return; + } + throw error; + } + }, 30000); + + it('should resolve coordinate systems', async () => { + try { + const sdata = await readZarr(fixtureUrl); + + // Should be able to get coordinate systems + const coordinateSystems = sdata.coordinateSystems; + expect(Array.isArray(coordinateSystems)).toBe(true); + + // The blobs dataset should have at least one coordinate system + // ('global') + expect(coordinateSystems.length).toBeGreaterThan(0); + } catch (error) { + if (error instanceof Error && error.message.includes('fetch')) { + console.warn('Skipping test - test server not running'); + return; + } + throw error; + } + }, 30000); + + it('should have valid string representation', async () => { + try { + const sdata = await readZarr(fixtureUrl); + + const str = sdata.toString(); + expect(typeof str).toBe('string'); + expect(str.length).toBeGreaterThan(0); + expect(str).toContain('SpatialData object'); + expect(str).toContain(fixtureUrl); + } catch (error) { + if (error instanceof Error && error.message.includes('fetch')) { + console.warn('Skipping test - test server not running'); + return; + } + throw error; + } + }, 30000); +}); + +describe('Fixture Generation', () => { + it('should generate fixtures for both versions', () => { + const v050Path = join(projectRoot, 'test-fixtures', 'v0.5.0', 'blobs.zarr'); + const v061Path = join(projectRoot, 'test-fixtures', 'v0.6.1', 'blobs.zarr'); + + // Try to generate if missing + if (!existsSync(v050Path)) { + ensureFixtures('0.5.0'); + } + if (!existsSync(v061Path)) { + ensureFixtures('0.6.1'); + } + + // Check that directories exist (even if generation failed, we want to know) + expect(existsSync(join(projectRoot, 'test-fixtures', 'v0.5.0'))).toBe(true); + expect(existsSync(join(projectRoot, 'test-fixtures', 'v0.6.1'))).toBe(true); + }, 60000); // 60 second timeout for generating both versions +}); + diff --git a/tests/unit/schemas.test.ts b/tests/unit/schemas.test.ts new file mode 100644 index 0000000..60ba980 --- /dev/null +++ b/tests/unit/schemas.test.ts @@ -0,0 +1,303 @@ +import { describe, expect, it } from 'vitest'; +import { + rasterAttrsSchema, + coordinateTransformationSchema, + shapesAttrsSchema, + pointsAttrsSchema, + tableAttrsSchema, + spatialDataSchema, +} from '../../packages/core/src/schemas/index.js'; + +describe('Schema Transformations', () => { + describe('rasterAttrsSchema - version normalization', () => { + it('should normalize v0.6.1 format (nested under ome) to internal format', () => { + const v061Format = { + ome: { + multiscales: [ + { + name: 'test', + datasets: [{ path: '0' }], + axes: [ + { name: 'y', type: 'space' }, + { name: 'x', type: 'space' }, + ], + }, + ], + }, + spatialdata_attrs: { + version: '0.6.1', + }, + }; + + const result = rasterAttrsSchema.parse(v061Format); + + // Should have multiscales at top level after transformation + expect(result.multiscales).toBeDefined(); + expect(result.multiscales).toHaveLength(1); + expect(result.multiscales[0].name).toBe('test'); + // Should not have 'ome' key in result + expect('ome' in result).toBe(false); + }); + + it('should accept v0.5.0 format (top-level multiscales) as-is', () => { + const v050Format = { + multiscales: [ + { + name: 'test', + datasets: [{ path: '0' }], + axes: [ + { name: 'y', type: 'space' }, + { name: 'x', type: 'space' }, + ], + }, + ], + spatialdata_attrs: { + version: '0.5.0', + }, + }; + + const result = rasterAttrsSchema.parse(v050Format); + + // Should have multiscales at top level + expect(result.multiscales).toBeDefined(); + expect(result.multiscales).toHaveLength(1); + expect(result.multiscales[0].name).toBe('test'); + }); + + it('should preserve omero data from v0.6.1 format', () => { + const v061Format = { + ome: { + multiscales: [ + { + name: 'test', + datasets: [{ path: '0' }], + axes: [ + { name: 'y', type: 'space' }, + { name: 'x', type: 'space' }, + ], + }, + ], + omero: { + channels: [ + { + label: 'channel1', + color: 'FF0000', + }, + ], + }, + }, + spatialdata_attrs: { + version: '0.6.1', + }, + }; + + const result = rasterAttrsSchema.parse(v061Format); + + expect(result.omero).toBeDefined(); + expect(result.omero?.channels).toHaveLength(1); + expect(result.omero?.channels[0].label).toBe('channel1'); + }); + }); + + describe('coordinateTransformationSchema', () => { + it('should validate scale transformation', () => { + const transform = [ + { + type: 'scale' as const, + scale: [1.0, 2.0, 3.0], + }, + ]; + + expect(() => coordinateTransformationSchema.parse(transform)).not.toThrow(); + const result = coordinateTransformationSchema.parse(transform); + expect(result[0].type).toBe('scale'); + }); + + it('should validate translation transformation', () => { + const transform = [ + { + type: 'translation' as const, + translation: [10.0, 20.0], + }, + ]; + + expect(() => coordinateTransformationSchema.parse(transform)).not.toThrow(); + }); + + it('should validate affine transformation', () => { + const transform = [ + { + type: 'affine' as const, + affine: [ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1], + ], + }, + ]; + + expect(() => coordinateTransformationSchema.parse(transform)).not.toThrow(); + }); + + it('should validate identity transformation', () => { + const transform = [ + { + type: 'identity' as const, + }, + ]; + + expect(() => coordinateTransformationSchema.parse(transform)).not.toThrow(); + }); + + it('should validate sequence transformation', () => { + const transform = [ + { + type: 'sequence' as const, + transformations: [ + { type: 'scale' as const, scale: [2.0, 2.0] }, + { type: 'translation' as const, translation: [10.0, 10.0] }, + ], + }, + ]; + + expect(() => coordinateTransformationSchema.parse(transform)).not.toThrow(); + }); + + it('should validate transformations with coordinate system references', () => { + const transform = [ + { + type: 'scale' as const, + scale: [1.0, 2.0], + input: { + name: 'input_cs', + axes: [ + { name: 'x', type: 'space' as const }, + { name: 'y', type: 'space' as const }, + ], + }, + output: { + name: 'output_cs', + }, + }, + ]; + + expect(() => coordinateTransformationSchema.parse(transform)).not.toThrow(); + const result = coordinateTransformationSchema.parse(transform); + expect(result[0].input?.name).toBe('input_cs'); + expect(result[0].output?.name).toBe('output_cs'); + }); + + it('should reject empty transformation array', () => { + const transform: unknown[] = []; + + expect(() => coordinateTransformationSchema.parse(transform)).toThrow(); + }); + }); + + describe('shapesAttrsSchema', () => { + it('should validate shapes attrs with transformations', () => { + const attrs = { + 'encoding-type': 'ngff:shapes', + axes: ['x', 'y'], + coordinateTransformations: [ + { + type: 'scale' as const, + scale: [1.0, 1.0], + }, + ], + spatialdata_attrs: { + version: '0.6.1', + }, + }; + + expect(() => shapesAttrsSchema.parse(attrs)).not.toThrow(); + }); + + it('should accept shapes attrs without transformations', () => { + const attrs = { + 'encoding-type': 'ngff:shapes', + axes: ['x', 'y'], + }; + + expect(() => shapesAttrsSchema.parse(attrs)).not.toThrow(); + }); + }); + + describe('pointsAttrsSchema', () => { + it('should validate points attrs with transformations', () => { + const attrs = { + 'encoding-type': 'ngff:points', + axes: ['x', 'y'], + coordinateTransformations: [ + { + type: 'translation' as const, + translation: [10.0, 20.0], + }, + ], + spatialdata_attrs: { + version: '0.6.1', + }, + }; + + expect(() => pointsAttrsSchema.parse(attrs)).not.toThrow(); + }); + }); + + describe('tableAttrsSchema', () => { + it('should validate table attrs', () => { + const attrs = { + instance_key: 'cell_id', + region: 'shapes', + region_key: 'region_id', + 'spatialdata-encoding-type': 'ngff:regions_table', + }; + + expect(() => tableAttrsSchema.parse(attrs)).not.toThrow(); + }); + + it('should accept array region', () => { + const attrs = { + instance_key: 'cell_id', + region: ['shapes1', 'shapes2'], + region_key: 'region_id', + 'spatialdata-encoding-type': 'ngff:regions_table', + }; + + expect(() => tableAttrsSchema.parse(attrs)).not.toThrow(); + }); + }); + + describe('spatialDataSchema', () => { + it('should validate spatial data root metadata', () => { + const metadata = { + version: '0.1.0', + coordinateSystems: { + global: [ + { + type: 'affine' as const, + affine: [ + [1, 0, 0], + [0, 1, 0], + ], + }, + ], + }, + }; + + expect(() => spatialDataSchema.parse(metadata)).not.toThrow(); + const result = spatialDataSchema.parse(metadata); + expect(result.version).toBe('0.1.0'); + expect(result.coordinateSystems.global).toBeDefined(); + }); + + it('should reject invalid version type', () => { + const metadata = { + version: 123, // should be string + coordinateSystems: {}, + }; + + expect(() => spatialDataSchema.parse(metadata)).toThrow(); + }); + }); +}); + diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..d0bc54f --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from 'vitest/config'; +import { resolve } from 'node:path'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['tests/**/*.test.ts'], + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + include: ['packages/**/*.ts'], + exclude: ['packages/**/*.test.ts', 'packages/**/dist/**'], + }, + }, + resolve: { + alias: { + '@spatialdata/core': resolve(__dirname, 'packages/core/src'), + '@spatialdata/zarrextra': resolve(__dirname, 'packages/zarrextra/src'), + }, + }, +}); + From f72cee54f9c9e76c07d8d3c99dea2e3e4d51586f Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Mon, 15 Dec 2025 11:14:48 +0000 Subject: [PATCH 07/50] add validation scripts based on datasets listed on spatialdata website --- .gitignore | 1 + README.md | 105 ++++++ package.json | 5 + packages/zarrextra/src/index.ts | 4 + python/scripts/validate_datasets.py | 490 ++++++++++++++++++++++++++++ scripts/validate-all.sh | 142 ++++++++ scripts/validate-datasets-js.js | 361 ++++++++++++++++++++ 7 files changed, 1108 insertions(+) create mode 100755 python/scripts/validate_datasets.py create mode 100755 scripts/validate-all.sh create mode 100755 scripts/validate-datasets-js.js diff --git a/.gitignore b/.gitignore index 050601d..2540208 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ build/ # Testing coverage/ test-fixtures/ +validation-results/ # Python virtual environments (version-specific, completely separate) python/v0.5.0/.venv/ diff --git a/README.md b/README.md index efdff78..7577a7c 100644 --- a/README.md +++ b/README.md @@ -206,6 +206,111 @@ const sdata = await readZarr('http://localhost:8081/?url=https://example.com/myd **⚠️ Warning:** The CORS proxy is for local development only. It has no security restrictions and should never be exposed to the internet. +### Dataset Validation + +The project includes scripts to validate dataset compatibility across different versions of the spatialdata library and the JavaScript implementation. + +#### Validating with Python + +Test publicly available datasets with both Python versions (0.5.0 and 0.6.1): + +```bash +# Validate all datasets with both Python versions +pnpm validate:datasets + +# Validate with a specific version only +pnpm validate:datasets:0.5.0 +pnpm validate:datasets:0.6.1 + +# Validate a specific dataset +pnpm validate:datasets -- --dataset "Xenium" + +# Output to a file +pnpm validate:datasets -- --output-file validation-results.md + +# Generate CSV output +pnpm validate:datasets -- --output-format csv --output-file results.csv + +# Generate JSON output (useful for programmatic comparison) +pnpm validate:datasets -- --output-format json --output-file results.json + +# Control parallel processing (default: 4 workers) +pnpm validate:datasets -- --workers 8 +pnpm validate:datasets -- --no-parallel # Sequential processing + +# Show detailed progress (verbose mode) +pnpm validate:datasets -- --verbose +``` + +**Performance Note:** The validation uses parallel processing by default (4 workers) to speed things up. Most time is spent importing the spatialdata library rather than downloading datasets. You can adjust the number of workers with `--workers N` or disable parallelism entirely with `--no-parallel`. + +#### Validating with JavaScript + +Test datasets with the JavaScript implementation in Node.js (outside browser): + +```bash +# First, build the packages +pnpm build + +# Validate all datasets with JS implementation +pnpm validate:datasets:js + +# Validate a specific dataset +pnpm validate:datasets:js -- --dataset "Xenium" + +# Use the CORS proxy (make sure it's running first: pnpm test:proxy) +pnpm validate:datasets:js -- --use-proxy + +# Output to a file +pnpm validate:datasets:js -- --output-file validation-results-js.md + +# Compare with Python results +pnpm validate:datasets -- --output-format json --output-file python-results.json +pnpm validate:datasets:js -- --compare-python python-results.json --output-file comparison.md +``` + +**Note:** The JavaScript validation may require the CORS proxy for datasets without CORS headers. Start it with `pnpm test:proxy` before running the validation. + +#### Understanding the Results + +The validation scripts generate a table showing which datasets work with each version: + +- ✅ Success: Dataset loaded successfully +- ❌ Failed: Dataset could not be loaded +- ⏭️ Not tested: Dataset was skipped + +The detailed results include: +- Element types present (images, labels, points, shapes, tables) +- Coordinate systems +- Error messages for failures + +This is useful for: +- Understanding baseline compatibility before testing the JS implementation +- Identifying version-specific issues +- Tracking which datasets are known to work or fail + +#### Comprehensive Validation Workflow + +For a complete validation of all datasets across all implementations, use the all-in-one workflow: + +```bash +# Run complete validation workflow +# This will: +# 1. Test all datasets with Python v0.5.0 and v0.6.1 +# 2. Build the packages (if needed) +# 3. Test all datasets with JavaScript +# 4. Generate comparison reports +pnpm validate:all +``` + +Results are saved in `validation-results//` with: +- `python-results.md` - Python validation results +- `comparison-report.md` - Side-by-side comparison of Python and JS results +- `python-results.json` - Raw Python results (for programmatic use) +- `js-results.json` - Raw JavaScript results (for programmatic use) + +A symlink `validation-results/latest/` always points to the most recent run. + ## 📝 License diff --git a/package.json b/package.json index 28cfc59..ff44388 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,11 @@ "test:fixtures:generate:0.6.1": "uv run python/scripts/generate_fixtures.py --version 0.6.1", "test:server": "node scripts/test-server.js", "test:proxy": "node scripts/cors-proxy.js", + "validate:datasets": "uv run python/scripts/validate_datasets.py", + "validate:datasets:0.5.0": "uv run python/scripts/validate_datasets.py --version 0.5.0", + "validate:datasets:0.6.1": "uv run python/scripts/validate_datasets.py --version 0.6.1", + "validate:datasets:js": "node scripts/validate-datasets-js.js", + "validate:all": "bash scripts/validate-all.sh", "lint": "biome check .", "format": "biome format --write .", "format:check": "biome format .", diff --git a/packages/zarrextra/src/index.ts b/packages/zarrextra/src/index.ts index 03cc247..59a43db 100644 --- a/packages/zarrextra/src/index.ts +++ b/packages/zarrextra/src/index.ts @@ -38,6 +38,10 @@ async function parseStoreContents(store: IntermediateConsolidatedStore): Promise }); // Open root for resolving array paths later + // this is throwing when I try to open http://localhost:8081/?url=https://s3.embl.de/spatialdata/spatialdata-sandbox/xenium_rep2_io.zarr + // it gets to `open_group_v2`, `throw new NodeNotFoundError("v2 group", ...)` + // ... but also, if I open that sample with python spatialdata 0.6.1 or 0.5.0, it also fails, in different ways. + // thinking about compiling a table of results from the examples on the website... const root = await zarr.open(store, { kind: 'group' }); const tree: ZarrTree = {}; diff --git a/python/scripts/validate_datasets.py b/python/scripts/validate_datasets.py new file mode 100755 index 0000000..23bf4ce --- /dev/null +++ b/python/scripts/validate_datasets.py @@ -0,0 +1,490 @@ +#!/usr/bin/env python3 +""" +Validate spatialdata dataset compatibility across different versions. + +This script tests loading publicly available spatialdata datasets from +spatialdata.scverse.org against different versions of the spatialdata library +to establish a baseline of what works with each version. +""" + +import argparse +import json +import subprocess +import sys +from dataclasses import dataclass, asdict +from pathlib import Path +from typing import Optional +from datetime import datetime +from multiprocessing import Pool, cpu_count + + +@dataclass +class ValidationResult: + """Result of validating a dataset with a specific version.""" + dataset_name: str + dataset_url: str + spatialdata_version: str + success: bool + error_type: Optional[str] = None + error_message: Optional[str] = None + elements: Optional[dict] = None + coordinate_systems: Optional[list] = None + + def to_dict(self): + """Convert to dictionary for JSON serialization.""" + return asdict(self) + + +# Dataset definitions from https://spatialdata.scverse.org/en/stable/tutorials/notebooks/datasets/README.html +DATASETS = [ + { + "name": "Visium HD (Mouse intestin)", + "url": "https://s3.embl.de/spatialdata/spatialdata-sandbox/visium_hd_3.0.0_io.zarr/", + }, + { + "name": "Visium (Breast cancer)", + "url": "https://s3.embl.de/spatialdata/spatialdata-sandbox/visium_associated_xenium_io.zarr/", + }, + { + "name": "Xenium (Breast cancer - Rep1)", + "url": "https://s3.embl.de/spatialdata/spatialdata-sandbox/xenium_rep1_io.zarr/", + }, + { + "name": "Xenium (Breast cancer - Rep2)", + "url": "https://s3.embl.de/spatialdata/spatialdata-sandbox/xenium_rep2_io.zarr/", + }, + { + "name": "CyCIF (Lung adenocarcinoma)", + "url": "https://s3.embl.de/spatialdata/spatialdata-sandbox/mcmicro_io.zarr/", + }, + { + "name": "MERFISH (Mouse brain)", + "url": "https://s3.embl.de/spatialdata/spatialdata-sandbox/merfish.zarr/", + }, + { + "name": "MIBI-TOF (Colorectal carcinoma)", + "url": "https://s3.embl.de/spatialdata/spatialdata-sandbox/mibitof.zarr/", + }, + { + "name": "Imaging Mass Cytometry (Multiple cancers)", + "url": "https://s3.embl.de/spatialdata/spatialdata-sandbox/steinbock_io.zarr/", + }, + { + "name": "Molecular Cartography (Mouse Liver)", + "url": "https://s3.embl.de/spatialdata/spatialdata-sandbox/mouse_liver.zarr", + }, + { + "name": "SpaceM (Hepa/NIH3T3 cells)", + "url": "https://s3.embl.de/spatialdata/spatialdata-sandbox/spacem_helanih3t3.zarr", + }, +] + + +def validate_with_version(dataset: dict, version: str, project_root: Path, verbose: bool = False) -> ValidationResult: + """ + Validate a dataset with a specific spatialdata version. + + This runs a subprocess with the version-specific environment to test loading the dataset. + """ + env_dir = project_root / "python" / f"v{version}" + + if verbose: + print(f" Loading spatialdata library (v{version})...", file=sys.stderr, flush=True) + + # Create a temporary Python script to run in the version-specific environment + test_script = f""" +import sys +import json +import spatialdata as sd + +def test_load(): + try: + print("Loading dataset...", file=sys.stderr, flush=True) + # Try to read the dataset + sdata = sd.read_zarr("{dataset['url']}") + + # Extract basic info + elements = {{}} + for element_type in ["images", "labels", "points", "shapes", "tables"]: + if hasattr(sdata, element_type): + attr = getattr(sdata, element_type) + if isinstance(attr, dict): + elements[element_type] = list(attr.keys()) + elif attr is not None: + elements[element_type] = True + + # Get coordinate systems + coordinate_systems = None + if hasattr(sdata, "coordinate_systems"): + cs = sdata.coordinate_systems + if isinstance(cs, dict): + coordinate_systems = list(cs.keys()) + elif isinstance(cs, list): + coordinate_systems = cs + elif cs is not None: + coordinate_systems = [str(cs)] + + result = {{ + "success": True, + "elements": elements, + "coordinate_systems": coordinate_systems, + }} + print(json.dumps(result)) + + except Exception as e: + error_type = type(e).__name__ + error_message = str(e) + result = {{ + "success": False, + "error_type": error_type, + "error_message": error_message, + }} + print(json.dumps(result)) + sys.exit(1) + +if __name__ == "__main__": + test_load() +""" + + # Run the test script in the version-specific environment + try: + result = subprocess.run( + ["uv", "run", "--directory", str(env_dir), "python", "-c", test_script], + capture_output=True, + text=True, + timeout=120, # 2 minute timeout per dataset + cwd=project_root, + ) + + # Parse the JSON output + try: + output = json.loads(result.stdout.strip().split('\n')[-1]) + return ValidationResult( + dataset_name=dataset["name"], + dataset_url=dataset["url"], + spatialdata_version=version, + success=output.get("success", False), + error_type=output.get("error_type"), + error_message=output.get("error_message"), + elements=output.get("elements"), + coordinate_systems=output.get("coordinate_systems"), + ) + except (json.JSONDecodeError, IndexError) as e: + # If we couldn't parse the output, something went wrong + return ValidationResult( + dataset_name=dataset["name"], + dataset_url=dataset["url"], + spatialdata_version=version, + success=False, + error_type="ParseError", + error_message=f"Could not parse output: {result.stdout}\nStderr: {result.stderr}", + ) + + except subprocess.TimeoutExpired: + return ValidationResult( + dataset_name=dataset["name"], + dataset_url=dataset["url"], + spatialdata_version=version, + success=False, + error_type="TimeoutError", + error_message="Dataset loading timed out after 120 seconds", + ) + except Exception as e: + return ValidationResult( + dataset_name=dataset["name"], + dataset_url=dataset["url"], + spatialdata_version=version, + success=False, + error_type=type(e).__name__, + error_message=str(e), + ) + + +def generate_markdown_table(results: list[ValidationResult]) -> str: + """Generate a markdown table from validation results.""" + + # Group results by dataset + datasets = {} + for result in results: + if result.dataset_name not in datasets: + datasets[result.dataset_name] = {} + datasets[result.dataset_name][result.spatialdata_version] = result + + # Generate table + lines = [] + lines.append("# SpatialData Dataset Compatibility Report") + lines.append(f"\nGenerated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") + lines.append("## Summary") + lines.append("") + lines.append("| Dataset | v0.5.0 | v0.6.1 | URL |") + lines.append("|---------|--------|--------|-----|") + + for dataset_name in sorted(datasets.keys()): + versions = datasets[dataset_name] + v050 = versions.get("0.5.0") + v061 = versions.get("0.6.1") + + v050_status = "✅" if v050 and v050.success else "❌" if v050 else "⏭️" + v061_status = "✅" if v061 and v061.success else "❌" if v061 else "⏭️" + + # Get URL from first available result + url = (v050 or v061).dataset_url if (v050 or v061) else "" + url_short = url.split("spatialdata-sandbox/")[-1] if "spatialdata-sandbox/" in url else url + + lines.append(f"| {dataset_name} | {v050_status} | {v061_status} | `{url_short}` |") + + lines.append("") + lines.append("Legend: ✅ Success | ❌ Failed | ⏭️ Not tested") + lines.append("") + + # Add detailed error information + lines.append("## Detailed Results") + lines.append("") + + for dataset_name in sorted(datasets.keys()): + versions = datasets[dataset_name] + lines.append(f"### {dataset_name}") + lines.append("") + + for version in ["0.5.0", "0.6.1"]: + result = versions.get(version) + if not result: + continue + + lines.append(f"#### spatialdata v{version}") + lines.append("") + + if result.success: + lines.append(f"**Status:** ✅ Success") + lines.append("") + + if result.elements: + lines.append("**Elements:**") + for element_type, items in result.elements.items(): + if isinstance(items, list): + lines.append(f"- {element_type}: {', '.join(items)}") + else: + lines.append(f"- {element_type}: present") + lines.append("") + + if result.coordinate_systems: + lines.append(f"**Coordinate Systems:** {', '.join(result.coordinate_systems)}") + lines.append("") + else: + lines.append(f"**Status:** ❌ Failed") + lines.append("") + lines.append(f"**Error Type:** `{result.error_type}`") + lines.append("") + lines.append(f"**Error Message:**") + lines.append("```") + lines.append(result.error_message or "No error message") + lines.append("```") + lines.append("") + + lines.append("---") + lines.append("") + + return "\n".join(lines) + + +def generate_csv_table(results: list[ValidationResult]) -> str: + """Generate a CSV table from validation results.""" + import csv + import io + + output = io.StringIO() + writer = csv.writer(output) + + # Write header + writer.writerow([ + "Dataset Name", + "Dataset URL", + "SpatialData Version", + "Success", + "Error Type", + "Error Message", + "Elements", + "Coordinate Systems", + ]) + + # Write rows + for result in results: + elements_str = json.dumps(result.elements) if result.elements else "" + cs_str = json.dumps(result.coordinate_systems) if result.coordinate_systems else "" + + writer.writerow([ + result.dataset_name, + result.dataset_url, + result.spatialdata_version, + result.success, + result.error_type or "", + result.error_message or "", + elements_str, + cs_str, + ]) + + return output.getvalue() + + +def validate_single(args_tuple): + """Helper function for multiprocessing pool.""" + dataset, version, project_root, verbose = args_tuple + return validate_with_version(dataset, version, project_root, verbose) + + +def main(): + parser = argparse.ArgumentParser( + description="Validate spatialdata dataset compatibility across versions" + ) + parser.add_argument( + "--version", + type=str, + choices=["0.5.0", "0.6.1"], + default=None, + help="SpatialData version to test (default: both)", + ) + parser.add_argument( + "--dataset", + type=str, + default=None, + help="Specific dataset name to test (default: all)", + ) + parser.add_argument( + "--output-format", + type=str, + choices=["markdown", "csv", "json"], + default="markdown", + help="Output format (default: markdown)", + ) + parser.add_argument( + "--output-file", + type=str, + default=None, + help="Output file path (default: print to stdout)", + ) + parser.add_argument( + "--workers", + type=int, + default=min(4, cpu_count()), + help=f"Number of parallel workers (default: min(4, {cpu_count()}))", + ) + parser.add_argument( + "--no-parallel", + action="store_true", + help="Disable parallel processing (run sequentially)", + ) + parser.add_argument( + "--verbose", + action="store_true", + help="Show detailed progress information", + ) + + args = parser.parse_args() + + # Get project root + project_root = Path(__file__).parent.parent.parent + + # Filter datasets if specific one requested + datasets = DATASETS + if args.dataset: + datasets = [d for d in DATASETS if args.dataset.lower() in d["name"].lower()] + if not datasets: + print(f"Error: No dataset matching '{args.dataset}' found", file=sys.stderr) + print("\nAvailable datasets:", file=sys.stderr) + for d in DATASETS: + print(f" - {d['name']}", file=sys.stderr) + sys.exit(1) + + # Determine versions to test + versions = [args.version] if args.version else ["0.5.0", "0.6.1"] + + # Ensure environments are set up + for version in versions: + env_dir = project_root / "python" / f"v{version}" + print(f"Setting up environment for spatialdata {version}...", file=sys.stderr) + result = subprocess.run( + ["uv", "sync", "--directory", str(env_dir)], + cwd=project_root, + capture_output=True, + ) + if result.returncode != 0: + print(f"Error setting up environment for version {version}", file=sys.stderr) + print(result.stderr.decode(), file=sys.stderr) + sys.exit(1) + + # Run validation + results = [] + total = len(datasets) * len(versions) + + print(f"\nValidating {len(datasets)} dataset(s) with {len(versions)} version(s)...", file=sys.stderr) + + if args.no_parallel: + print("Running sequentially (parallel processing disabled)", file=sys.stderr) + else: + print(f"Using {args.workers} parallel worker(s)", file=sys.stderr) + + print("Note: Most time is spent importing spatialdata, not downloading datasets", file=sys.stderr) + print("", file=sys.stderr) + + # Prepare arguments for validation + validation_tasks = [ + (dataset, version, project_root, args.verbose) + for dataset in datasets + for version in versions + ] + + if args.no_parallel: + # Sequential processing + for i, task in enumerate(validation_tasks): + dataset, version, project_root, verbose = task + print(f"[{i+1}/{total}] Testing {dataset['name']} with spatialdata v{version}...", file=sys.stderr, flush=True) + + result = validate_with_version(dataset, version, project_root, verbose) + results.append(result) + + status = "✅" if result.success else "❌" + print(f" {status} {dataset['name']} (v{version})", file=sys.stderr, flush=True) + if not result.success: + print(f" Error: {result.error_type}", file=sys.stderr, flush=True) + print("", file=sys.stderr, flush=True) + else: + # Parallel processing using process pool + print("Starting validation pool...", file=sys.stderr, flush=True) + print("", file=sys.stderr, flush=True) + + with Pool(processes=args.workers) as pool: + # Use imap to get results as they complete + for i, result in enumerate(pool.imap(validate_single, validation_tasks)): + results.append(result) + + status = "✅" if result.success else "❌" + print(f"[{i+1}/{total}] {status} {result.dataset_name} (v{result.spatialdata_version})", file=sys.stderr, flush=True) + if not result.success and args.verbose: + print(f" Error: {result.error_type}", file=sys.stderr, flush=True) + + print("", file=sys.stderr) + print("Validation complete!", file=sys.stderr) + print("", file=sys.stderr) + + # Generate output + if args.output_format == "markdown": + output = generate_markdown_table(results) + elif args.output_format == "csv": + output = generate_csv_table(results) + elif args.output_format == "json": + output = json.dumps([r.to_dict() for r in results], indent=2) + else: + output = "" + + # Write output + if args.output_file: + output_path = Path(args.output_file) + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(output) + print(f"Results written to: {output_path}", file=sys.stderr) + else: + print(output) + + +if __name__ == "__main__": + main() diff --git a/scripts/validate-all.sh b/scripts/validate-all.sh new file mode 100755 index 0000000..c2a6d05 --- /dev/null +++ b/scripts/validate-all.sh @@ -0,0 +1,142 @@ +#!/bin/bash + +# Comprehensive dataset validation workflow +# This script runs all validation tests and generates a comparison report + +set -e # Exit on error + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Output directory +OUTPUT_DIR="validation-results" +TIMESTAMP=$(date +%Y%m%d-%H%M%S) +RUN_DIR="$OUTPUT_DIR/$TIMESTAMP" + +echo -e "${BLUE}================================${NC}" +echo -e "${BLUE}Dataset Validation Workflow${NC}" +echo -e "${BLUE}================================${NC}" +echo "" + +# Create output directory +mkdir -p "$RUN_DIR" +echo -e "${GREEN}Output directory: $RUN_DIR${NC}" +echo "" + +# Step 1: Validate with Python (both versions) +echo -e "${YELLOW}Step 1/3: Validating with Python (v0.5.0 and v0.6.1)${NC}" +echo "This will test 10 datasets x 2 versions = 20 tests" +echo "Note: Most time is spent importing spatialdata, not downloading" +echo "Using parallel processing to speed things up..." +echo "" + +uv run python/scripts/validate_datasets.py \ + --output-format json \ + --output-file "$RUN_DIR/python-results.json" > "$RUN_DIR/python-output.txt" 2>&1 || { + echo -e "${RED}Python validation failed. Check $RUN_DIR/python-output.txt for details.${NC}" + exit 1 +} + +# Also generate markdown for Python results +uv run python/scripts/validate_datasets.py \ + --output-format markdown \ + --output-file "$RUN_DIR/python-results.md" > /dev/null 2>&1 + +echo -e "${GREEN}✓ Python validation complete${NC}" +echo "" + +# Step 2: Build the project if needed +if [ ! -d "packages/core/dist" ]; then + echo -e "${YELLOW}Step 2/3: Building packages${NC}" + echo "Building packages for JavaScript validation..." + pnpm build > "$RUN_DIR/build-output.txt" 2>&1 || { + echo -e "${RED}Build failed. Check $RUN_DIR/build-output.txt for details.${NC}" + exit 1 + } + echo -e "${GREEN}✓ Build complete${NC}" + echo "" +else + echo -e "${YELLOW}Step 2/3: Packages already built${NC}" + echo -e "${GREEN}✓ Skipping build${NC}" + echo "" +fi + +# Step 3: Validate with JavaScript +echo -e "${YELLOW}Step 3/3: Validating with JavaScript${NC}" +echo "Testing datasets with JS implementation..." +echo "" + +node scripts/validate-datasets-js.js \ + --output-format json \ + --output-file "$RUN_DIR/js-results.json" > "$RUN_DIR/js-output.txt" 2>&1 || { + echo -e "${RED}JavaScript validation failed. Check $RUN_DIR/js-output.txt for details.${NC}" + exit 1 +} + +# Generate comparison report +node scripts/validate-datasets-js.js \ + --compare-python "$RUN_DIR/python-results.json" \ + --output-format markdown \ + --output-file "$RUN_DIR/comparison-report.md" > /dev/null 2>&1 + +echo -e "${GREEN}✓ JavaScript validation complete${NC}" +echo "" + +# Generate summary +echo -e "${BLUE}================================${NC}" +echo -e "${BLUE}Validation Summary${NC}" +echo -e "${BLUE}================================${NC}" +echo "" + +# Count results from JSON files +PYTHON_SUCCESS=$(jq '[.[] | select(.success == true)] | length' "$RUN_DIR/python-results.json") +PYTHON_TOTAL=$(jq 'length' "$RUN_DIR/python-results.json") +JS_SUCCESS=$(jq '[.[] | select(.success == true)] | length' "$RUN_DIR/js-results.json") +JS_TOTAL=$(jq 'length' "$RUN_DIR/js-results.json") + +echo -e "Python (both versions): ${GREEN}${PYTHON_SUCCESS}/${PYTHON_TOTAL}${NC} successful" +echo -e "JavaScript: ${GREEN}${JS_SUCCESS}/${JS_TOTAL}${NC} successful" +echo "" + +echo -e "${BLUE}Generated Reports:${NC}" +echo -e " - Python results: ${RUN_DIR}/python-results.md" +echo -e " - JavaScript results: ${RUN_DIR}/comparison-report.md" +echo -e " - Raw JSON (Python): ${RUN_DIR}/python-results.json" +echo -e " - Raw JSON (JS): ${RUN_DIR}/js-results.json" +echo "" + +# Create a symlink to latest results +rm -f "$OUTPUT_DIR/latest" +ln -s "$TIMESTAMP" "$OUTPUT_DIR/latest" +echo -e "${GREEN}✓ Symlink created: $OUTPUT_DIR/latest${NC}" +echo "" + +# Display a preview of failures +PYTHON_FAILURES=$(jq -r '.[] | select(.success == false) | "\(.dataset_name) (v\(.spatialdata_version)): \(.error_type)"' "$RUN_DIR/python-results.json" | head -5) +JS_FAILURES=$(jq -r '.[] | select(.success == false) | "\(.datasetName): \(.errorType)"' "$RUN_DIR/js-results.json" | head -5) + +if [ -n "$PYTHON_FAILURES" ]; then + echo -e "${RED}Python Failures (showing first 5):${NC}" + echo "$PYTHON_FAILURES" + echo "" +fi + +if [ -n "$JS_FAILURES" ]; then + echo -e "${RED}JavaScript Failures (showing first 5):${NC}" + echo "$JS_FAILURES" + echo "" +fi + +echo -e "${GREEN}================================${NC}" +echo -e "${GREEN}Validation workflow complete!${NC}" +echo -e "${GREEN}================================${NC}" +echo "" +echo -e "View the comparison report:" +echo -e " cat $RUN_DIR/comparison-report.md" +echo "" +echo -e "Or open in your markdown viewer:" +echo -e " open $RUN_DIR/comparison-report.md" diff --git a/scripts/validate-datasets-js.js b/scripts/validate-datasets-js.js new file mode 100755 index 0000000..156809b --- /dev/null +++ b/scripts/validate-datasets-js.js @@ -0,0 +1,361 @@ +#!/usr/bin/env node +/** + * Validate spatialdata dataset compatibility with the JavaScript implementation. + * + * This script tests loading publicly available spatialdata datasets using the + * @spatialdata/core library in a Node.js environment (outside the browser). + */ + +import { readZarr } from '../packages/core/dist/index.js'; +import { writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +// Dataset definitions from https://spatialdata.scverse.org/en/stable/tutorials/notebooks/datasets/README.html +const DATASETS = [ + { + name: "Visium HD (Mouse intestin)", + url: "https://s3.embl.de/spatialdata/spatialdata-sandbox/visium_hd_3.0.0_io.zarr/", + }, + { + name: "Visium (Breast cancer)", + url: "https://s3.embl.de/spatialdata/spatialdata-sandbox/visium_associated_xenium_io.zarr/", + }, + { + name: "Xenium (Breast cancer - Rep1)", + url: "https://s3.embl.de/spatialdata/spatialdata-sandbox/xenium_rep1_io.zarr/", + }, + { + name: "Xenium (Breast cancer - Rep2)", + url: "https://s3.embl.de/spatialdata/spatialdata-sandbox/xenium_rep2_io.zarr/", + }, + { + name: "CyCIF (Lung adenocarcinoma)", + url: "https://s3.embl.de/spatialdata/spatialdata-sandbox/mcmicro_io.zarr/", + }, + { + name: "MERFISH (Mouse brain)", + url: "https://s3.embl.de/spatialdata/spatialdata-sandbox/merfish.zarr/", + }, + { + name: "MIBI-TOF (Colorectal carcinoma)", + url: "https://s3.embl.de/spatialdata/spatialdata-sandbox/mibitof.zarr/", + }, + { + name: "Imaging Mass Cytometry (Multiple cancers)", + url: "https://s3.embl.de/spatialdata/spatialdata-sandbox/steinbock_io.zarr/", + }, + { + name: "Molecular Cartography (Mouse Liver)", + url: "https://s3.embl.de/spatialdata/spatialdata-sandbox/mouse_liver.zarr", + }, + { + name: "SpaceM (Hepa/NIH3T3 cells)", + url: "https://s3.embl.de/spatialdata/spatialdata-sandbox/spacem_helanih3t3.zarr", + }, +]; + +/** + * Validate a single dataset + */ +async function validateDataset(dataset, useProxy = false) { + const result = { + datasetName: dataset.name, + datasetUrl: dataset.url, + implementation: 'JavaScript (@spatialdata/core)', + success: false, + errorType: null, + errorMessage: null, + elements: null, + coordinateSystems: null, + }; + + try { + // Use proxy if requested + const url = useProxy + ? `http://localhost:8081/?url=${encodeURIComponent(dataset.url)}` + : dataset.url; + + // Try to read the dataset + const sdata = await readZarr(url); + + // Extract basic info + const elements = {}; + for (const elementType of ['images', 'labels', 'points', 'shapes', 'tables']) { + if (sdata[elementType]) { + elements[elementType] = Object.keys(sdata[elementType]); + } + } + + // Get coordinate systems + let coordinateSystems = null; + if (sdata.coordinateSystems) { + if (Array.isArray(sdata.coordinateSystems)) { + coordinateSystems = sdata.coordinateSystems; + } else { + coordinateSystems = [sdata.coordinateSystems]; + } + } + + result.success = true; + result.elements = elements; + result.coordinateSystems = coordinateSystems; + } catch (error) { + result.success = false; + result.errorType = error.constructor.name; + result.errorMessage = error.message; + + // Include stack trace for debugging + if (error.stack) { + result.stackTrace = error.stack.split('\n').slice(0, 5).join('\n'); + } + } + + return result; +} + +/** + * Generate a markdown table from validation results + */ +function generateMarkdownTable(results, pythonResults = null) { + const lines = []; + + lines.push('# SpatialData Dataset Compatibility Report (JavaScript)'); + lines.push(`\nGenerated: ${new Date().toISOString()}\n`); + lines.push('## Summary'); + lines.push(''); + + // If we have Python results, show comparison + if (pythonResults) { + lines.push('| Dataset | JS | Python v0.5.0 | Python v0.6.1 | URL |'); + lines.push('|---------|-------|---------------|---------------|-----|'); + + const pythonResultsByDataset = {}; + for (const r of pythonResults) { + if (!pythonResultsByDataset[r.dataset_name]) { + pythonResultsByDataset[r.dataset_name] = {}; + } + pythonResultsByDataset[r.dataset_name][r.spatialdata_version] = r; + } + + for (const result of results) { + const jsStatus = result.success ? '✅' : '❌'; + const py050 = pythonResultsByDataset[result.datasetName]?.['0.5.0']; + const py061 = pythonResultsByDataset[result.datasetName]?.['0.6.1']; + const py050Status = py050 ? (py050.success ? '✅' : '❌') : '⏭️'; + const py061Status = py061 ? (py061.success ? '✅' : '❌') : '⏭️'; + + const urlShort = result.datasetUrl.split('spatialdata-sandbox/')[1] || result.datasetUrl; + lines.push(`| ${result.datasetName} | ${jsStatus} | ${py050Status} | ${py061Status} | \`${urlShort}\` |`); + } + } else { + lines.push('| Dataset | Status | URL |'); + lines.push('|---------|--------|-----|'); + + for (const result of results) { + const status = result.success ? '✅' : '❌'; + const urlShort = result.datasetUrl.split('spatialdata-sandbox/')[1] || result.datasetUrl; + lines.push(`| ${result.datasetName} | ${status} | \`${urlShort}\` |`); + } + } + + lines.push(''); + lines.push('Legend: ✅ Success | ❌ Failed | ⏭️ Not tested'); + lines.push(''); + + // Add detailed error information + lines.push('## Detailed Results (JavaScript)'); + lines.push(''); + + for (const result of results) { + lines.push(`### ${result.datasetName}`); + lines.push(''); + + if (result.success) { + lines.push('**Status:** ✅ Success'); + lines.push(''); + + if (result.elements && Object.keys(result.elements).length > 0) { + lines.push('**Elements:**'); + for (const [elementType, items] of Object.entries(result.elements)) { + if (Array.isArray(items) && items.length > 0) { + lines.push(`- ${elementType}: ${items.join(', ')}`); + } else { + lines.push(`- ${elementType}: present`); + } + } + lines.push(''); + } + + if (result.coordinateSystems && result.coordinateSystems.length > 0) { + lines.push(`**Coordinate Systems:** ${result.coordinateSystems.join(', ')}`); + lines.push(''); + } + } else { + lines.push('**Status:** ❌ Failed'); + lines.push(''); + lines.push(`**Error Type:** \`${result.errorType}\``); + lines.push(''); + lines.push('**Error Message:**'); + lines.push('```'); + lines.push(result.errorMessage || 'No error message'); + lines.push('```'); + lines.push(''); + + if (result.stackTrace) { + lines.push('**Stack Trace:**'); + lines.push('```'); + lines.push(result.stackTrace); + lines.push('```'); + lines.push(''); + } + } + + lines.push('---'); + lines.push(''); + } + + return lines.join('\n'); +} + +/** + * Parse command line arguments + */ +function parseArgs() { + const args = { + dataset: null, + outputFormat: 'markdown', + outputFile: null, + useProxy: false, + comparePython: null, + }; + + for (let i = 2; i < process.argv.length; i++) { + const arg = process.argv[i]; + + if (arg === '--dataset' && i + 1 < process.argv.length) { + args.dataset = process.argv[++i]; + } else if (arg === '--output-format' && i + 1 < process.argv.length) { + args.outputFormat = process.argv[++i]; + } else if (arg === '--output-file' && i + 1 < process.argv.length) { + args.outputFile = process.argv[++i]; + } else if (arg === '--use-proxy') { + args.useProxy = true; + } else if (arg === '--compare-python' && i + 1 < process.argv.length) { + args.comparePython = process.argv[++i]; + } else if (arg === '--help') { + console.log(` +Usage: node validate-datasets-js.js [options] + +Options: + --dataset Test only dataset matching this name + --output-format Output format: markdown, csv, json (default: markdown) + --output-file Write output to file instead of stdout + --use-proxy Use CORS proxy (http://localhost:8081) + --compare-python Compare with Python results JSON file + --help Show this help message + `); + process.exit(0); + } + } + + return args; +} + +/** + * Main function + */ +async function main() { + const args = parseArgs(); + + // Filter datasets if specific one requested + let datasets = DATASETS; + if (args.dataset) { + datasets = DATASETS.filter(d => + d.name.toLowerCase().includes(args.dataset.toLowerCase()) + ); + + if (datasets.length === 0) { + console.error(`Error: No dataset matching '${args.dataset}' found`); + console.error('\nAvailable datasets:'); + for (const d of DATASETS) { + console.error(` - ${d.name}`); + } + process.exit(1); + } + } + + // Warn if using proxy + if (args.useProxy) { + console.error('Using CORS proxy at http://localhost:8081'); + console.error('Make sure the proxy is running: pnpm test:proxy'); + console.error(''); + } + + // Run validation + console.error(`Validating ${datasets.length} dataset(s) with JavaScript implementation...\n`); + + const results = []; + let current = 0; + + for (const dataset of datasets) { + current++; + console.error(`[${current}/${datasets.length}] Testing ${dataset.name}...`); + + const result = await validateDataset(dataset, args.useProxy); + results.push(result); + + const status = result.success ? '✅' : '❌'; + console.error(` ${status} ${dataset.name}`); + if (!result.success) { + console.error(` Error: ${result.errorType}`); + } + } + + console.error('\nValidation complete!\n'); + + // Load Python results if requested + let pythonResults = null; + if (args.comparePython) { + try { + const fs = await import('node:fs'); + const data = fs.readFileSync(args.comparePython, 'utf-8'); + pythonResults = JSON.parse(data); + console.error(`Loaded Python results from ${args.comparePython}\n`); + } catch (error) { + console.error(`Warning: Could not load Python results: ${error.message}`); + } + } + + // Generate output + let output; + if (args.outputFormat === 'markdown') { + output = generateMarkdownTable(results, pythonResults); + } else if (args.outputFormat === 'csv') { + // Simple CSV generation + const lines = ['Dataset Name,Dataset URL,Implementation,Success,Error Type,Error Message,Elements,Coordinate Systems']; + for (const r of results) { + const elements = r.elements ? JSON.stringify(r.elements) : ''; + const cs = r.coordinateSystems ? JSON.stringify(r.coordinateSystems) : ''; + lines.push(`"${r.datasetName}","${r.datasetUrl}","${r.implementation}",${r.success},"${r.errorType || ''}","${r.errorMessage || ''}","${elements}","${cs}"`); + } + output = lines.join('\n'); + } else if (args.outputFormat === 'json') { + output = JSON.stringify(results, null, 2); + } else { + console.error(`Unknown output format: ${args.outputFormat}`); + process.exit(1); + } + + // Write output + if (args.outputFile) { + writeFileSync(resolve(args.outputFile), output); + console.error(`Results written to: ${args.outputFile}`); + } else { + console.log(output); + } +} + +main().catch(error => { + console.error('Fatal error:', error); + process.exit(1); +}); From 1488aa43e3f07ecc00c5f04e23bea5426d0b4ab6 Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Mon, 15 Dec 2025 13:15:58 +0000 Subject: [PATCH 08/50] lint fixtures.tests.ts --- tests/integration/fixtures.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration/fixtures.test.ts b/tests/integration/fixtures.test.ts index 06cf5a4..04c714c 100644 --- a/tests/integration/fixtures.test.ts +++ b/tests/integration/fixtures.test.ts @@ -76,8 +76,8 @@ describe.each(versions)('Integration Tests - spatialdata v%s', (version) => { // we'll skip this test with a helpful message if (error instanceof Error && error.message.includes('fetch')) { console.warn( - `Skipping integration test - test server not running. ` + - `Start it with: pnpm test:server` + 'Skipping integration test - test server not running. ' + + 'Start it with: pnpm test:server' ); return; } @@ -90,7 +90,7 @@ describe.each(versions)('Integration Tests - spatialdata v%s', (version) => { const sdata = await readZarr(fixtureUrl); // Check that we can access parsed structure - expect(sdata.parsed).toBeDefined(); + expect(sdata.rootStore.tree).toBeDefined(); // The blobs dataset should have at least images // We'll check what elements are available From 12246d0a4823b993fc1077111854600d37ef4fdb Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Mon, 15 Dec 2025 13:20:50 +0000 Subject: [PATCH 09/50] normalise baseUrl to fix issue with trailing slash --- packages/zarrextra/src/index.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/zarrextra/src/index.ts b/packages/zarrextra/src/index.ts index 59a43db..dfbe69d 100644 --- a/packages/zarrextra/src/index.ts +++ b/packages/zarrextra/src/index.ts @@ -235,9 +235,13 @@ function normalizeV2ToV3Metadata(v2Metadata: ZarrV2Metadata): ZarrV3Metadata { async function tryConsolidated(store: zarr.FetchStore): Promise { //!!! nb - we need to also handle local files, in which case we don't fetch(url), we need another method - this is important + // Normalize base URL to avoid double slashes when constructing metadata paths + const urlString = typeof store.url === 'string' ? store.url : store.url.toString(); + const baseUrl = urlString.replace(/\/+$/, ''); + // First, try zarr.json (v3 format) try { - const zarrJsonPath = `${store.url}/zarr.json`; + const zarrJsonPath = `${baseUrl}/zarr.json`; const zarrJson = await (await fetch(zarrJsonPath)).json(); const parseResult = await parseZarrJson(zarrJson); @@ -258,7 +262,7 @@ async function tryConsolidated(store: zarr.FetchStore): Promise Date: Mon, 15 Dec 2025 14:27:10 +0000 Subject: [PATCH 10/50] pnpm github action config --- .github/workflows/test.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2c659cc..7ba9e77 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,13 +23,16 @@ jobs: node-version: '20' cache: 'pnpm' + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + run_install: false + - uses: astral-sh/setup-uv@v4 with: version: "latest" - - name: Install pnpm - run: npm install -g pnpm - - name: Install dependencies run: pnpm install From 2c93bdfe28f8e0dcfe08bcac97df706f08bd2ab7 Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Mon, 15 Dec 2025 14:28:42 +0000 Subject: [PATCH 11/50] node version in github action --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7ba9e77..31273bb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' cache: 'pnpm' - name: Setup pnpm From 4873cd2a524f69f7c601841e39acd819fdb4f9c1 Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Mon, 15 Dec 2025 14:38:35 +0000 Subject: [PATCH 12/50] remove misplaced `cache: 'pnpm'` in test action --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 31273bb..a156cec 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,7 +21,6 @@ jobs: - uses: actions/setup-node@v4 with: node-version: '22' - cache: 'pnpm' - name: Setup pnpm uses: pnpm/action-setup@v4 From c3814b1258617b6c548255947cd91a3b15ba1433 Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Mon, 15 Dec 2025 14:49:48 +0000 Subject: [PATCH 13/50] remove erroneous 'test-fixtures' from urls in integration test fixtures. --- tests/integration/fixtures.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration/fixtures.test.ts b/tests/integration/fixtures.test.ts index 04c714c..4dadc4c 100644 --- a/tests/integration/fixtures.test.ts +++ b/tests/integration/fixtures.test.ts @@ -60,7 +60,7 @@ describe.each(versions)('Integration Tests - spatialdata v%s', (version) => { // For now, we'll need to use a workaround since FetchStore expects HTTP URLs // We'll use a local server URL pattern that the test server would serve // In practice, tests should start the test server first - fixtureUrl = `http://localhost:8080/test-fixtures/v${version}/blobs.zarr`; + fixtureUrl = `http://localhost:8080/v${version}/blobs.zarr`; }); it('should load spatialdata store', async () => { @@ -152,8 +152,8 @@ describe.each(versions)('Integration Tests - spatialdata v%s', (version) => { describe('Fixture Generation', () => { it('should generate fixtures for both versions', () => { - const v050Path = join(projectRoot, 'test-fixtures', 'v0.5.0', 'blobs.zarr'); - const v061Path = join(projectRoot, 'test-fixtures', 'v0.6.1', 'blobs.zarr'); + const v050Path = join(projectRoot, 'v0.5.0', 'blobs.zarr'); + const v061Path = join(projectRoot, 'v0.6.1', 'blobs.zarr'); // Try to generate if missing if (!existsSync(v050Path)) { From 8b874f2c69f61140586682927df5ee616cdab87e Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Mon, 15 Dec 2025 14:52:15 +0000 Subject: [PATCH 14/50] disable integration tests in CI --- .github/workflows/test.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a156cec..d210557 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -47,6 +47,7 @@ jobs: - name: Run unit tests run: pnpm test:unit - - name: Run integration tests - run: pnpm test:integration + # note: these tests can be run manually for now - CI doesn't have necessary localhost:8080 server setup + # - name: Run integration tests + # run: pnpm test:integration From e0dbb07e274877c97b8f4c93496e67c6d2010700 Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Mon, 15 Dec 2025 15:04:07 +0000 Subject: [PATCH 15/50] try to configure CI with localhost for test fixtures --- .github/workflows/test.yml | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d210557..d5b1656 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -47,7 +47,25 @@ jobs: - name: Run unit tests run: pnpm test:unit - # note: these tests can be run manually for now - CI doesn't have necessary localhost:8080 server setup - # - name: Run integration tests - # run: pnpm test:integration + - name: Run integration tests with local server + shell: bash + run: | + # Start test server in background + pnpm test:server & + SERVER_PID=$! + # Wait for server to be ready (up to ~30s) + for i in {1..30}; do + if curl -sSf http://localhost:8080/ >/dev/null; then + echo "Test server is up" + break + fi + echo "Waiting for test server on http://localhost:8080/ ..." + sleep 1 + done + + # Run integration tests (will hit http://localhost:8080/…) + pnpm test:integration + + # Clean up server + kill "$SERVER_PID" || true \ No newline at end of file From 58b62f223215be0bd75e57be3ef8c408bff1b840 Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Mon, 15 Dec 2025 15:16:39 +0000 Subject: [PATCH 16/50] longer hookTimeout for test fixture generation. remove some dead code/comments. --- tests/integration/fixtures.test.ts | 19 ------------------- vitest.config.ts | 2 ++ 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/tests/integration/fixtures.test.ts b/tests/integration/fixtures.test.ts index 4dadc4c..9db1e6d 100644 --- a/tests/integration/fixtures.test.ts +++ b/tests/integration/fixtures.test.ts @@ -34,20 +34,6 @@ function ensureFixtures(version: string): string { return fixturePath; } -/** - * Get the URL for a fixture (for use with readZarr) - * In a real scenario, this would be served by the test server - * For now, we'll use file:// URLs if the environment supports it, - * or we'll need to start a local server - */ -function getFixtureUrl(fixturePath: string): string { - // For now, we'll use a file:// URL - // Note: This may not work with FetchStore, which expects HTTP URLs - // In a real scenario, we'd start the test server and use http://localhost:8080/... - // For now, we'll test with a local path that might work with future local file support - return `file://${fixturePath}`; -} - // Test matrix for different spatialdata versions const versions = ['0.5.0', '0.6.1'] as const; @@ -57,16 +43,11 @@ describe.each(versions)('Integration Tests - spatialdata v%s', (version) => { beforeAll(() => { fixturePath = ensureFixtures(version); - // For now, we'll need to use a workaround since FetchStore expects HTTP URLs - // We'll use a local server URL pattern that the test server would serve - // In practice, tests should start the test server first fixtureUrl = `http://localhost:8080/v${version}/blobs.zarr`; }); it('should load spatialdata store', async () => { // Note: This test requires the test server to be running - // In CI, we'd start it as part of the test setup - // For now, we'll skip if the server isn't available try { const sdata = await readZarr(fixtureUrl); expect(sdata).toBeDefined(); diff --git a/vitest.config.ts b/vitest.config.ts index d0bc54f..274df48 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -6,6 +6,8 @@ export default defineConfig({ globals: true, environment: 'node', include: ['tests/**/*.test.ts'], + // Integration tests may need extra time for fixture generation hooks + hookTimeout: 60000, coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], From 55d97642a30bcad8d109e01e66b6280f8b9ab4cc Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Mon, 15 Dec 2025 16:39:10 +0000 Subject: [PATCH 17/50] possibly overcomplicated changes to allow proxy server to work --- packages/zarrextra/src/index.ts | 40 +++++++++++++++++++++++++++++---- scripts/cors-proxy.js | 34 +++++++++++----------------- 2 files changed, 49 insertions(+), 25 deletions(-) diff --git a/packages/zarrextra/src/index.ts b/packages/zarrextra/src/index.ts index dfbe69d..9ae39fa 100644 --- a/packages/zarrextra/src/index.ts +++ b/packages/zarrextra/src/index.ts @@ -101,6 +101,38 @@ async function parseStoreContents(store: IntermediateConsolidatedStore): Promise return tree; } +/** + * Build a metadata URL for a given base store URL. + * + * Handles both direct URLs (e.g. https://.../dataset.zarr) and the dev + * CORS proxy pattern (e.g. http://localhost:8081/?url=https://.../dataset.zarr). + * + * For the proxy pattern, we append the metadata path to the *inner* URL in the + * `url` search parameter, instead of to the proxy URL itself. + */ +function buildMetadataUrl(baseUrl: string, metadataPath: string): string { + try { + const outer = new URL(baseUrl); + const innerUrl = outer.searchParams.get('url'); + + if (innerUrl) { + // Dev proxy case: http://proxy/?url=https://host/store.zarr + const inner = new URL(innerUrl); + const innerBase = inner.toString().replace(/\/+$/, ''); + const innerWithMetadata = `${innerBase}/${metadataPath}`; + + outer.searchParams.set('url', innerWithMetadata); + return outer.toString(); + } + } catch { + // If baseUrl isn't a valid absolute URL, fall back to simple joining below. + } + + // Non‑proxy (or unparseable) case: just append the metadata path. + const trimmed = baseUrl.replace(/\/+$/, ''); + return `${trimmed}/${metadataPath}`; +} + /** * Parse zarr v3 consolidated metadata from zarr.json * The actual zarr v3 structure has metadata nested under consolidated_metadata.metadata @@ -237,11 +269,11 @@ async function tryConsolidated(store: zarr.FetchStore): Promise Date: Mon, 15 Dec 2025 19:25:12 +0000 Subject: [PATCH 22/50] escape html characters in directory listing scripts/test-server.js Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- scripts/test-server.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/scripts/test-server.js b/scripts/test-server.js index 040dc65..99a1284 100755 --- a/scripts/test-server.js +++ b/scripts/test-server.js @@ -40,13 +40,25 @@ function getMimeType(filePath) { /** * Generate HTML directory listing */ +/** + * Escape HTML special characters + */ +function escapeHtml(str) { + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} + function generateDirectoryListing(files, currentPath, basePath) { const items = files .map((file) => { const isDir = file.isDirectory; - const href = `${currentPath}/${file.name}${isDir ? '/' : ''}`; + const safeName = escapeHtml(file.name); + const href = `${currentPath}/${encodeURIComponent(file.name)}${isDir ? '/' : ''}`; const icon = isDir ? '📁' : '📄'; - return `
  • ${icon} ${file.name}
  • `; + return `
  • ${icon} ${safeName}
  • `; }) .join('\n'); From 891cc1ffe1edd90116af7c3f19b88c89d9a7c29b Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Tue, 16 Dec 2025 11:01:16 +0000 Subject: [PATCH 23/50] add some unused python spatialdata_integrity checking stuff --- python/spatialdata-integrity/.gitignore | 43 ++ python/spatialdata-integrity/README.md | 96 ++++ python/spatialdata-integrity/example_usage.py | 166 ++++++ python/spatialdata-integrity/pyproject.toml | 28 + .../src/spatialdata_integrity/__init__.py | 21 + .../src/spatialdata_integrity/checker.py | 541 ++++++++++++++++++ .../src/spatialdata_integrity/cli.py | 145 +++++ .../spatialdata-integrity/test_known_good.py | 67 +++ 8 files changed, 1107 insertions(+) create mode 100644 python/spatialdata-integrity/.gitignore create mode 100644 python/spatialdata-integrity/README.md create mode 100644 python/spatialdata-integrity/example_usage.py create mode 100644 python/spatialdata-integrity/pyproject.toml create mode 100644 python/spatialdata-integrity/src/spatialdata_integrity/__init__.py create mode 100644 python/spatialdata-integrity/src/spatialdata_integrity/checker.py create mode 100644 python/spatialdata-integrity/src/spatialdata_integrity/cli.py create mode 100644 python/spatialdata-integrity/test_known_good.py diff --git a/python/spatialdata-integrity/.gitignore b/python/spatialdata-integrity/.gitignore new file mode 100644 index 0000000..bbca041 --- /dev/null +++ b/python/spatialdata-integrity/.gitignore @@ -0,0 +1,43 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual environments +.venv/ +venv/ +ENV/ +env/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# Testing +.pytest_cache/ +.coverage +htmlcov/ + +# OS +.DS_Store +Thumbs.db + diff --git a/python/spatialdata-integrity/README.md b/python/spatialdata-integrity/README.md new file mode 100644 index 0000000..b9e6315 --- /dev/null +++ b/python/spatialdata-integrity/README.md @@ -0,0 +1,96 @@ +# spatialdata-integrity + +Utilities for checking the integrity of SpatialData Zarr stores, detecting corrupt chunks, and validating data completeness. + +This is - as of now - a somewhat throwaway AI generated and untested set of scripts that might be useful later. I'm not sure if they should live here, but I wanted something I could potentially get back to later, and possibly publish to pypi if it's turned into anything more substantial. + +During development and testing of various permutations of SpatialData object and library versions, at some point I had an error saving an object which at first I wrongly attributed to a `spatialdata` bug - but appears to have been because I had some partial copy of the object I was working on. + + +## Features + +- **Zarr Array Integrity Checking**: Verify that all chunks in zarr arrays can be decompressed and read +- **Blosc Decompression Validation**: Detect corrupt blosc-compressed chunks +- **SpatialData Object Validation**: Check all elements (images, labels, points, shapes, tables) in a SpatialData object +- **Progress Reporting**: Detailed reporting of which elements/chunks are problematic +- **CLI Tool**: Easy-to-use command-line interface + +## Installation + +The package can be installed in development mode for use in both SpatialData.js and MDV projects: + +```bash +# Using uv (recommended) +cd python/spatialdata-integrity +uv pip install -e . + +# Or using pip +cd python/spatialdata-integrity +pip install -e . +``` + +For use in a specific project's environment (e.g., SpatialData.ts v0.6.1): + +```bash +# Install in SpatialData.ts v0.6.1 environment +cd python/v0.6.1 +uv pip install -e ../spatialdata-integrity + +# Or in MDV +cd python +pip install -e ../SpatialData.ts/python/spatialdata-integrity +``` + +## Usage + +### CLI + +```bash +# Check a SpatialData zarr store +check-spatialdata /path/to/spatialdata.zarr + +# Verbose output +check-spatialdata /path/to/spatialdata.zarr --verbose + +# Check only specific element types +check-spatialdata /path/to/spatialdata.zarr --elements images labels + +# Output results to JSON +check-spatialdata /path/to/spatialdata.zarr --output results.json +``` + +### Python API + +```python +from spatialdata_integrity import check_spatialdata, check_zarr_array +import spatialdata as sd + +# Check a full SpatialData object +sdata = sd.read_zarr("path/to/spatialdata.zarr") +results = check_spatialdata(sdata) +if results.is_valid: + print("All checks passed!") +else: + print(f"Found {len(results.errors)} errors") + for error in results.errors: + print(f" - {error}") + +# Check a specific zarr array +import zarr +arr = zarr.open("path/to/array.zarr") +results = check_zarr_array(arr) +``` + +## Example Output + +``` +Checking SpatialData object: /path/to/spatialdata.zarr +✓ Images: 'image1' (3 chunks checked) +✓ Labels: 'labels1' (12 chunks checked) +✗ Images: 'image2' - Blosc decompression error at chunk (0, 0, 0) +✓ Points: 'points1' (1 chunk checked) +✓ Tables: 'table1' (validated) + +Summary: 1 error found in 1 element +``` + diff --git a/python/spatialdata-integrity/example_usage.py b/python/spatialdata-integrity/example_usage.py new file mode 100644 index 0000000..2ca539f --- /dev/null +++ b/python/spatialdata-integrity/example_usage.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +""" +Example usage of spatialdata-integrity checker. + +This demonstrates how to use the integrity checker both programmatically +and via the CLI. +""" + +import sys +from pathlib import Path + +# Add src to path for development +sys.path.insert(0, str(Path(__file__).parent / "src")) + +from spatialdata_integrity import check_spatialdata, check_zarr_array +import spatialdata as sd + + +def example_check_spatialdata(): + """Example: Check a full SpatialData object.""" + print("=" * 60) + print("Example 1: Checking a SpatialData object") + print("=" * 60) + + # Replace with your path + path = "path/to/your/spatialdata.zarr" + + if not Path(path).exists(): + print(f"Path does not exist: {path}") + print("Please update the path in this script.") + return + + try: + result = check_spatialdata(path, verbose=True) + print("\n" + str(result)) + + if result.is_valid: + print("\n✓ All checks passed!") + else: + print("\n✗ Found errors:") + for element in result.elements: + if not element.is_valid: + print(f" - {element.element_type} '{element.element_name}':") + for error in element.errors: + print(f" * Chunk {error.chunk_index}: {error.error_type}") + print(f" {error.error_message}") + except Exception as e: + print(f"Error: {e}") + import traceback + traceback.print_exc() + + +def example_check_specific_elements(): + """Example: Check only specific element types.""" + print("\n" + "=" * 60) + print("Example 2: Checking only images and labels") + print("=" * 60) + + path = "path/to/your/spatialdata.zarr" + + if not Path(path).exists(): + print(f"Path does not exist: {path}") + return + + try: + # Check only images and labels + result = check_spatialdata( + path, + element_types=["images", "labels"], + verbose=True + ) + print("\n" + str(result)) + except Exception as e: + print(f"Error: {e}") + + +def example_check_zarr_array(): + """Example: Check a specific zarr array.""" + print("\n" + "=" * 60) + print("Example 3: Checking a specific zarr array") + print("=" * 60) + + import zarr + + # Open a zarr array directly + array_path = "path/to/array.zarr" + + if not Path(array_path).exists(): + print(f"Path does not exist: {array_path}") + return + + try: + arr = zarr.open(array_path, mode="r") + result = check_zarr_array(arr, array_path=array_path) + + print(f"\nArray: {array_path}") + print(f"Shape: {arr.shape}") + print(f"Chunks: {arr.chunks}") + print(f"Chunks checked: {result.chunks_checked}") + + if result.is_valid: + print("✓ All chunks are valid") + else: + print("✗ Found errors:") + for error in result.errors: + print(f" - Chunk {error.chunk_index}: {error.error_type}") + print(f" {error.error_message}") + except Exception as e: + print(f"Error: {e}") + + +def example_find_corrupt_chunks(): + """Example: Find which specific chunks are corrupt.""" + print("\n" + "=" * 60) + print("Example 4: Finding corrupt chunks") + print("=" * 60) + + path = "path/to/your/spatialdata.zarr" + + if not Path(path).exists(): + print(f"Path does not exist: {path}") + return + + try: + result = check_spatialdata(path) + + # Find all corrupt chunks + corrupt_chunks = [] + for element in result.elements: + if not element.is_valid: + for error in element.errors: + corrupt_chunks.append({ + "element": f"{element.element_type}/{element.element_name}", + "chunk": error.chunk_index, + "error": error.error_type, + "message": error.error_message, + }) + + if corrupt_chunks: + print(f"\nFound {len(corrupt_chunks)} corrupt chunk(s):") + for chunk_info in corrupt_chunks: + print(f"\n Element: {chunk_info['element']}") + print(f" Chunk index: {chunk_info['chunk']}") + print(f" Error type: {chunk_info['error']}") + print(f" Message: {chunk_info['message']}") + else: + print("\n✓ No corrupt chunks found") + + except Exception as e: + print(f"Error: {e}") + + +if __name__ == "__main__": + print("SpatialData Integrity Checker - Usage Examples") + print("=" * 60) + print("\nNote: Update the paths in this script to point to your data.") + print("\nUncomment the example you want to run:\n") + + # Uncomment the example you want to run: + # example_check_spatialdata() + # example_check_specific_elements() + # example_check_zarr_array() + # example_find_corrupt_chunks() + + print("\nTo run examples, uncomment them in the script.") + diff --git a/python/spatialdata-integrity/pyproject.toml b/python/spatialdata-integrity/pyproject.toml new file mode 100644 index 0000000..c65e420 --- /dev/null +++ b/python/spatialdata-integrity/pyproject.toml @@ -0,0 +1,28 @@ +[project] +name = "spatialdata-integrity" +version = "0.1.0" +description = "Utilities for checking integrity of SpatialData Zarr stores" +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "zarr>=3.0.0", + "spatialdata>=0.5.0", + "numpy>=1.20.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=7.0.0", + "pytest-asyncio>=0.21.0", +] + +[project.scripts] +check-spatialdata = "spatialdata_integrity.cli:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/spatialdata_integrity"] + diff --git a/python/spatialdata-integrity/src/spatialdata_integrity/__init__.py b/python/spatialdata-integrity/src/spatialdata_integrity/__init__.py new file mode 100644 index 0000000..6d3eed3 --- /dev/null +++ b/python/spatialdata-integrity/src/spatialdata_integrity/__init__.py @@ -0,0 +1,21 @@ +""" +Utilities for checking integrity of SpatialData Zarr stores. +""" + +from .checker import ( + check_spatialdata, + check_zarr_array, + IntegrityResult, + ElementResult, + ChunkError, +) + +__version__ = "0.1.0" +__all__ = [ + "check_spatialdata", + "check_zarr_array", + "IntegrityResult", + "ElementResult", + "ChunkError", +] + diff --git a/python/spatialdata-integrity/src/spatialdata_integrity/checker.py b/python/spatialdata-integrity/src/spatialdata_integrity/checker.py new file mode 100644 index 0000000..e1c74d8 --- /dev/null +++ b/python/spatialdata-integrity/src/spatialdata_integrity/checker.py @@ -0,0 +1,541 @@ +""" +Core integrity checking functions for SpatialData and Zarr arrays. +""" + +from dataclasses import dataclass, field +from pathlib import Path +from typing import Optional, Union, List, Dict, Any +import traceback + +try: + import zarr + import numpy as np + import spatialdata as sd +except ImportError as e: + raise ImportError( + "spatialdata-integrity requires zarr, numpy, and spatialdata. " + f"Install them with: pip install zarr numpy spatialdata\n" + f"Original error: {e}" + ) + + +@dataclass +class ChunkError: + """Information about a corrupt chunk.""" + + chunk_index: tuple + error_type: str + error_message: str + array_path: Optional[str] = None + + +@dataclass +class ElementResult: + """Result of checking a single SpatialData element.""" + + element_type: str # 'images', 'labels', 'points', 'shapes', 'tables' + element_name: str + is_valid: bool + chunks_checked: int = 0 + errors: List[ChunkError] = field(default_factory=list) + warning: Optional[str] = None + + def __str__(self) -> str: + status = "✓" if self.is_valid else "✗" + msg = f"{status} {self.element_type.capitalize()}: '{self.element_name}'" + if self.chunks_checked > 0: + msg += f" ({self.chunks_checked} chunks checked)" + if self.warning: + msg += f" - Warning: {self.warning}" + if self.errors: + for error in self.errors: + msg += f"\n - Error at chunk {error.chunk_index}: {error.error_type}" + return msg + + +@dataclass +class IntegrityResult: + """Result of checking a SpatialData object.""" + + path: Optional[str] = None + is_valid: bool = True + elements: List[ElementResult] = field(default_factory=list) + errors: List[str] = field(default_factory=list) + + def __str__(self) -> str: + lines = [] + if self.path: + lines.append(f"Checking SpatialData object: {self.path}") + else: + lines.append("Checking SpatialData object") + + for element in self.elements: + lines.append(f" {element}") + + if self.errors: + lines.append("") + lines.append("Errors encountered:") + for error in self.errors: + lines.append(f" - {error}") + + lines.append("") + error_count = sum(1 for e in self.elements if not e.is_valid) + lines.append(f"Summary: {error_count} error(s) found in {len(self.elements)} element(s)") + + return "\n".join(lines) + + +def check_zarr_array( + array: zarr.Array, + sample_chunks: bool = True, + max_chunks_to_check: Optional[int] = None, + array_path: Optional[str] = None, +) -> ElementResult: + """ + Check the integrity of a zarr array by attempting to read chunks. + + Args: + array: The zarr array to check + sample_chunks: If True, sample chunks across the array. If False, check all chunks. + max_chunks_to_check: Maximum number of chunks to check (None = check all) + array_path: Optional path identifier for error reporting + + Returns: + ElementResult with validation results + """ + errors: List[ChunkError] = [] + chunks_checked = 0 + + try: + # Get array shape and chunks + shape = array.shape + chunks = array.chunks + + if chunks is None: + # No chunking, try to read a small slice + try: + # Read a small sample from the beginning + sample_shape = tuple(min(10, s) for s in shape) + _ = array[tuple(slice(0, s) for s in sample_shape)] + chunks_checked = 1 + except Exception as e: + error_type = type(e).__name__ + error_message = str(e) + errors.append( + ChunkError( + chunk_index=(0,) * len(shape), + error_type=error_type, + error_message=error_message, + array_path=array_path, + ) + ) + chunks_checked = 1 + else: + # Calculate chunk indices + chunk_indices = [] + for dim_size, chunk_size in zip(shape, chunks): + num_chunks = (dim_size + chunk_size - 1) // chunk_size + chunk_indices.append(list(range(num_chunks))) + + # Generate chunk coordinates to check + import itertools + + all_chunk_coords = list(itertools.product(*chunk_indices)) + + if sample_chunks and len(all_chunk_coords) > 10: + # Sample chunks: first, last, and a few in between + import random + + samples = [all_chunk_coords[0], all_chunk_coords[-1]] + if len(all_chunk_coords) > 2: + samples.extend( + random.sample( + all_chunk_coords[1:-1], + min(8, len(all_chunk_coords) - 2), + ) + ) + chunks_to_check = samples + else: + chunks_to_check = all_chunk_coords + + # Limit number of chunks if specified + if max_chunks_to_check is not None: + chunks_to_check = chunks_to_check[:max_chunks_to_check] + + # Check each chunk + for chunk_coords in chunks_to_check: + try: + # Calculate slice for this chunk + slices = [] + for coord, dim_size, chunk_size in zip(chunk_coords, shape, chunks): + start = coord * chunk_size + end = min(start + chunk_size, dim_size) + slices.append(slice(start, end)) + + # Try to read the chunk + _ = array[tuple(slices)] + chunks_checked += 1 + except Exception as e: + error_type = type(e).__name__ + error_message = str(e) + + # Check if it's a blosc decompression error + if "blosc" in error_message.lower() or "decompression" in error_message.lower(): + error_type = "BloscDecompressionError" + + errors.append( + ChunkError( + chunk_index=chunk_coords, + error_type=error_type, + error_message=error_message, + array_path=array_path, + ) + ) + chunks_checked += 1 + + except Exception as e: + # Error accessing array metadata + error_type = type(e).__name__ + error_message = str(e) + errors.append( + ChunkError( + chunk_index=(), + error_type=error_type, + error_message=error_message, + array_path=array_path, + ) + ) + + return ElementResult( + element_type="array", + element_name=array_path or "unknown", + is_valid=len(errors) == 0, + chunks_checked=chunks_checked, + errors=errors, + ) + + +def check_spatialdata_element( + element: Any, + element_type: str, + element_name: str, + verbose: bool = False, +) -> ElementResult: + """ + Check a single SpatialData element (image, label, points, shape, or table). + + Args: + element: The SpatialData element to check + element_type: Type of element ('images', 'labels', 'points', 'shapes', 'tables') + element_name: Name of the element + verbose: If True, provide more detailed output + + Returns: + ElementResult with validation results + """ + errors: List[ChunkError] = [] + chunks_checked = 0 + warning: Optional[str] = None + + try: + if element_type in ("images", "labels"): + # Images and labels are typically DataTree or similar with .data attribute + if hasattr(element, "data"): + data = element.data + + # Try to access as DataTree (multi-scale) + # DataTree objects can be iterated or accessed via .values() or items() + scale_levels = None + if hasattr(data, "values"): + # DataTree with .values() method + try: + scale_levels = list(data.values()) + except Exception: + pass + elif hasattr(data, "items"): + # DataTree with .items() method + try: + scale_levels = [(k, v) for k, v in data.items()] + except Exception: + pass + elif hasattr(data, "__iter__") and not isinstance(data, (str, bytes)): + # Try as iterable + try: + scale_levels = list(data) + except Exception: + pass + + if scale_levels: + # Multi-scale: check each scale level + for i, scale_item in enumerate(scale_levels): + if isinstance(scale_item, tuple): + scale_name, scale_data = scale_item + else: + scale_name = f"scale_{i}" + scale_data = scale_item + + # Try to get underlying zarr array from dask array + zarr_array = None + if hasattr(scale_data, "store"): + # Dask array with store attribute + try: + # Try to get zarr array from dask + if hasattr(scale_data.store, "array"): + zarr_array = scale_data.store.array + except Exception: + pass + + # If we have a zarr array, check it directly + if zarr_array is not None and hasattr(zarr_array, "chunks"): + result = check_zarr_array( + zarr_array, + array_path=f"{element_name}/{scale_name}", + ) + chunks_checked += result.chunks_checked + errors.extend(result.errors) + else: + # Try to read a small sample to trigger chunk access + try: + # Get shape and read a small slice + if hasattr(scale_data, "shape"): + shape = scale_data.shape + # Read first chunk or small slice + ndim = len(shape) + if ndim >= 2: + # Read a small 2D slice + slices = tuple(slice(0, min(10, s)) for s in shape[:2]) + if ndim > 2: + # For higher dims, take first index + slices = slices + tuple(0 for _ in range(ndim - 2)) + + # Try to access - this will trigger chunk loading + sample = scale_data[slices] + + # If it's a dask array, compute it + if hasattr(sample, "compute"): + _ = sample.compute() + + chunks_checked += 1 + else: + # 1D or 0D - just try to access + _ = scale_data[0] if shape[0] > 0 else scale_data + chunks_checked += 1 + else: + # No shape attribute - try basic access + _ = scale_data[0] if hasattr(scale_data, "__getitem__") else scale_data + chunks_checked += 1 + except Exception as e: + error_type = type(e).__name__ + error_message = str(e) + if "blosc" in error_message.lower() or "decompression" in error_message.lower(): + error_type = "BloscDecompressionError" + errors.append( + ChunkError( + chunk_index=(scale_name,), + error_type=error_type, + error_message=error_message, + array_path=f"{element_name}/{scale_name}", + ) + ) + chunks_checked += 1 + elif hasattr(data, "chunks"): + # Single zarr array (not multi-scale) + result = check_zarr_array(data, array_path=element_name) + chunks_checked += result.chunks_checked + errors.extend(result.errors) + else: + # Try to access directly as array-like + try: + if hasattr(data, "shape"): + shape = data.shape + if len(shape) >= 2: + slices = tuple(slice(0, min(10, s)) for s in shape[:2]) + sample = data[slices] + if hasattr(sample, "compute"): + _ = sample.compute() + chunks_checked += 1 + else: + _ = data[0] if shape[0] > 0 else data + chunks_checked += 1 + else: + warning = "Could not determine array structure - no shape attribute" + except Exception as e: + error_type = type(e).__name__ + error_message = str(e) + if "blosc" in error_message.lower(): + error_type = "BloscDecompressionError" + errors.append( + ChunkError( + chunk_index=(), + error_type=error_type, + error_message=error_message, + array_path=element_name, + ) + ) + chunks_checked += 1 + else: + warning = "Element has no 'data' attribute" + + elif element_type == "points": + # Points are typically dask DataFrames + # For now, just check if we can access metadata + try: + if hasattr(element, "compute"): + # Try a small sample + sample = element.head(10) + _ = sample.compute() if hasattr(sample, "compute") else sample + chunks_checked = 1 + else: + # Already computed or not dask + _ = element.head(10) + chunks_checked = 1 + except Exception as e: + error_type = type(e).__name__ + error_message = str(e) + errors.append( + ChunkError( + chunk_index=(), + error_type=error_type, + error_message=error_message, + array_path=element_name, + ) + ) + chunks_checked = 1 + + elif element_type == "shapes": + # Shapes are typically GeoDataFrames + # Just check if we can access them + try: + _ = len(element) + chunks_checked = 1 + except Exception as e: + error_type = type(e).__name__ + error_message = str(e) + errors.append( + ChunkError( + chunk_index=(), + error_type=error_type, + error_message=error_message, + array_path=element_name, + ) + ) + chunks_checked = 1 + + elif element_type == "tables": + # Tables are AnnData objects + try: + # Check if we can access the data + _ = element.shape + _ = element.X + chunks_checked = 1 + except Exception as e: + error_type = type(e).__name__ + error_message = str(e) + errors.append( + ChunkError( + chunk_index=(), + error_type=error_type, + error_message=error_message, + array_path=element_name, + ) + ) + chunks_checked = 1 + + except Exception as e: + error_type = type(e).__name__ + error_message = str(e) + errors.append( + ChunkError( + chunk_index=(), + error_type=error_type, + error_message=error_message, + array_path=element_name, + ) + ) + + return ElementResult( + element_type=element_type, + element_name=element_name, + is_valid=len(errors) == 0, + chunks_checked=chunks_checked, + errors=errors, + warning=warning, + ) + + +def check_spatialdata( + sdata: Union[sd.SpatialData, str, Path], + element_types: Optional[List[str]] = None, + verbose: bool = False, +) -> IntegrityResult: + """ + Check the integrity of a SpatialData object. + + Args: + sdata: SpatialData object or path to zarr store + element_types: Optional list of element types to check (e.g., ['images', 'labels']). + If None, checks all element types. + verbose: If True, provide more detailed output + + Returns: + IntegrityResult with validation results + """ + # Load SpatialData if path provided + if isinstance(sdata, (str, Path)): + path = str(sdata) + try: + sdata = sd.read_zarr(path) + except Exception as e: + return IntegrityResult( + path=path, + is_valid=False, + errors=[f"Failed to load SpatialData object: {type(e).__name__}: {e}"], + ) + else: + path = None + + if element_types is None: + element_types = ["images", "labels", "points", "shapes", "tables"] + + results: List[ElementResult] = [] + all_errors: List[str] = [] + + # Check each element type + for element_type in element_types: + if not hasattr(sdata, element_type): + continue + + elements = getattr(sdata, element_type) + if elements is None: + continue + + if isinstance(elements, dict): + # Multiple elements of this type + for element_name, element in elements.items(): + try: + result = check_spatialdata_element( + element, element_type, element_name, verbose=verbose + ) + results.append(result) + except Exception as e: + all_errors.append( + f"Error checking {element_type} '{element_name}': {type(e).__name__}: {e}" + ) + if verbose: + all_errors.append(traceback.format_exc()) + elif elements is not None: + # Single element (unlikely but possible) + result = check_spatialdata_element( + elements, element_type, element_type, verbose=verbose + ) + results.append(result) + + is_valid = len(all_errors) == 0 and all(r.is_valid for r in results) + + return IntegrityResult( + path=path, + is_valid=is_valid, + elements=results, + errors=all_errors, + ) + diff --git a/python/spatialdata-integrity/src/spatialdata_integrity/cli.py b/python/spatialdata-integrity/src/spatialdata_integrity/cli.py new file mode 100644 index 0000000..71e8ffe --- /dev/null +++ b/python/spatialdata-integrity/src/spatialdata_integrity/cli.py @@ -0,0 +1,145 @@ +""" +Command-line interface for spatialdata-integrity. +""" + +import argparse +import json +import sys +from pathlib import Path +from typing import Optional + +from .checker import check_spatialdata, IntegrityResult + + +def format_json_result(result: IntegrityResult) -> str: + """Format result as JSON.""" + output = { + "path": result.path, + "is_valid": result.is_valid, + "elements": [ + { + "element_type": e.element_type, + "element_name": e.element_name, + "is_valid": e.is_valid, + "chunks_checked": e.chunks_checked, + "errors": [ + { + "chunk_index": list(e.chunk_index), + "error_type": e.error_type, + "error_message": e.error_message, + "array_path": e.array_path, + } + for e in element.errors + ], + "warning": e.warning, + } + for e in result.elements + ], + "errors": result.errors, + } + return json.dumps(output, indent=2) + + +def main(): + """Main CLI entry point.""" + parser = argparse.ArgumentParser( + description="Check integrity of SpatialData Zarr stores", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Check a SpatialData store + check-spatialdata /path/to/spatialdata.zarr + + # Verbose output + check-spatialdata /path/to/spatialdata.zarr --verbose + + # Check only images and labels + check-spatialdata /path/to/spatialdata.zarr --elements images labels + + # Output to JSON file + check-spatialdata /path/to/spatialdata.zarr --output results.json + """, + ) + + parser.add_argument( + "path", + type=str, + help="Path to SpatialData Zarr store", + ) + + parser.add_argument( + "--elements", + nargs="+", + choices=["images", "labels", "points", "shapes", "tables"], + default=None, + help="Element types to check (default: all)", + ) + + parser.add_argument( + "--verbose", + "-v", + action="store_true", + help="Verbose output", + ) + + parser.add_argument( + "--output", + "-o", + type=str, + default=None, + help="Output file path (JSON format). If not specified, prints to stdout.", + ) + + parser.add_argument( + "--format", + choices=["text", "json"], + default="text", + help="Output format (default: text)", + ) + + args = parser.parse_args() + + # Check if path exists + path = Path(args.path) + if not path.exists(): + print(f"Error: Path does not exist: {path}", file=sys.stderr) + sys.exit(1) + + # Run integrity check + try: + result = check_spatialdata( + str(path), + element_types=args.elements, + verbose=args.verbose, + ) + except Exception as e: + print(f"Error: Failed to check SpatialData object: {e}", file=sys.stderr) + if args.verbose: + import traceback + + traceback.print_exc() + sys.exit(1) + + # Format output + if args.format == "json": + output = format_json_result(result) + else: + output = str(result) + + # Write output + if args.output: + output_path = Path(args.output) + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(output) + print(f"Results written to: {output_path}", file=sys.stderr) + else: + print(output) + + # Exit with error code if validation failed + if not result.is_valid: + sys.exit(1) + + +if __name__ == "__main__": + main() + diff --git a/python/spatialdata-integrity/test_known_good.py b/python/spatialdata-integrity/test_known_good.py new file mode 100644 index 0000000..3027d64 --- /dev/null +++ b/python/spatialdata-integrity/test_known_good.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +""" +Test script to verify integrity checking works with known-good data. + +This script tests the integrity checker with the blobs dataset, +which should be valid and not have any corrupt chunks. +""" + +import sys +from pathlib import Path + +# Add src to path for development +sys.path.insert(0, str(Path(__file__).parent / "src")) + +from spatialdata_integrity import check_spatialdata + + +def test_known_good(): + """Test with known-good blobs dataset.""" + # Try to find blobs dataset in test-fixtures + project_root = Path(__file__).parent.parent.parent + fixtures_dir = project_root / "test-fixtures" + + # Try v0.6.1 first, then v0.5.0 + test_paths = [ + fixtures_dir / "v0.6.1" / "blobs.zarr", + fixtures_dir / "v0.5.0" / "blobs.zarr", + ] + + test_path = None + for path in test_paths: + if path.exists(): + test_path = path + break + + if test_path is None: + print("Error: Could not find blobs.zarr in test-fixtures") + print(f"Looked in: {[str(p) for p in test_paths]}") + return 1 + + print(f"Testing with known-good dataset: {test_path}") + print("=" * 60) + + try: + result = check_spatialdata(str(test_path), verbose=True) + print("\n" + str(result)) + + if result.is_valid: + print("\n✓ All checks passed! The integrity checker is working correctly.") + return 0 + else: + print("\n✗ Found errors in known-good dataset. This may indicate:") + print(" 1. The dataset is actually corrupted") + print(" 2. There's a bug in the integrity checker") + return 1 + + except Exception as e: + print(f"\n✗ Error running integrity check: {e}") + import traceback + + traceback.print_exc() + return 1 + + +if __name__ == "__main__": + sys.exit(test_known_good()) + From 4805243195969faa3244059e1bf2ed158d8c359e Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Tue, 16 Dec 2025 11:13:49 +0000 Subject: [PATCH 24/50] capture normalizedPath used in zarrextra get closure --- packages/zarrextra/src/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/zarrextra/src/index.ts b/packages/zarrextra/src/index.ts index 88bbc40..0153aac 100644 --- a/packages/zarrextra/src/index.ts +++ b/packages/zarrextra/src/index.ts @@ -83,10 +83,12 @@ async function parseStoreContents(store: IntermediateConsolidatedStore): Promise storage_transformers: arrayNode.storage_transformers, }; + // Capture normalizedPath by value to avoid closure issues + const arrayPath = normalizedPath; currentNode[part] = { [ATTRS_KEY]: attrs, [ZARRAY_KEY]: zarray, - get: () => zarr.open(root.resolve(normalizedPath), { kind: 'array' }) + get: () => zarr.open(root.resolve(arrayPath), { kind: 'array' }) }; } else { // Group node From 9c08999b9dd84715db61c734286d1b1efd6f95b3 Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Tue, 27 Jan 2026 17:12:17 +0000 Subject: [PATCH 25/50] update to viv_19 with zarrita --- packages/vis/package.json | 2 +- .../vis/src/ImageView/avivatorish/state.tsx | 9 +- .../vis/src/ImageView/avivatorish/utils.ts | 5 +- packages/vis/src/ImageView/index.tsx | 2 +- pnpm-lock.yaml | 158 +++++++----------- pnpm-workspace.yaml | 2 +- 6 files changed, 73 insertions(+), 105 deletions(-) diff --git a/packages/vis/package.json b/packages/vis/package.json index f504ebb..ed4a8fc 100644 --- a/packages/vis/package.json +++ b/packages/vis/package.json @@ -35,7 +35,7 @@ "@spatialdata/react": "workspace:*", "@uidotdev/usehooks": "^2.4.1", "@uiw/react-json-view": "2.0.0-alpha.39", - "@vivjs-experimental/viv": "catalog:", + "@hms-dbmi/viv": "catalog:", "deck.gl": "catalog:", "geotiff": "2.1.4-beta.0", "zustand": "^5.0.8" diff --git a/packages/vis/src/ImageView/avivatorish/state.tsx b/packages/vis/src/ImageView/avivatorish/state.tsx index 057a1ea..5fe08e7 100644 --- a/packages/vis/src/ImageView/avivatorish/state.tsx +++ b/packages/vis/src/ImageView/avivatorish/state.tsx @@ -1,5 +1,5 @@ import { type PropsWithChildren, createContext, useContext } from "react"; -import type { loadBioformatsZarr, loadOmeTiff, loadOmeZarr } from "@vivjs-experimental/viv"; +import type { loadOmeTiff, loadOmeZarr } from "@hms-dbmi/viv"; import { createStore } from "zustand"; import { useStoreWithEqualityFn } from "zustand/traditional"; import type { EqFn, Selector, ZustandStore } from "./zustandTypes"; @@ -8,11 +8,10 @@ import type { EqFn, Selector, ZustandStore } from "./zustandTypes"; // ... not to mention HTJ2K? export type OME_TIFF = Awaited>; export type OME_ZARR = Awaited>; -export type BIO_ZARR = Awaited>; -export type PixelSource = OME_TIFF | OME_ZARR | BIO_ZARR; +export type PixelSource = OME_TIFF | OME_ZARR; // --- copied straight from Avivator's code::: with notes / changes for MDV (and now SpatialData.js) --- -import { RENDERING_MODES } from "@vivjs-experimental/viv"; +import { RENDERING_MODES } from "@hms-dbmi/viv"; const capitalize = (string: string) => string.charAt(0).toUpperCase() + string.slice(1); @@ -338,7 +337,7 @@ type OME_METADATA = OME_ZARR['metadata'] & { PhysicalSizeXUnit?: string; } } -export type Metadata = OME_TIFF['metadata'] | OME_METADATA | BIO_ZARR['metadata']; +export type Metadata = OME_TIFF['metadata'] | OME_METADATA; //export type Metadata = TiffPreviewProps["metadata"]; export const useMetadata = (): Metadata | undefined | null => { try { diff --git a/packages/vis/src/ImageView/avivatorish/utils.ts b/packages/vis/src/ImageView/avivatorish/utils.ts index d5a4771..6c612f9 100644 --- a/packages/vis/src/ImageView/avivatorish/utils.ts +++ b/packages/vis/src/ImageView/avivatorish/utils.ts @@ -3,15 +3,12 @@ import { fromBlob, fromUrl } from "geotiff"; import { Matrix4 } from "@math.gl/core"; import { - loadOmeTiff, - loadBioformatsZarr, loadOmeZarr, - loadMultiTiff, getChannelStats, RENDERING_MODES, ColorPalette3DExtensions, AdditiveColormap3DExtensions, -} from "@vivjs-experimental/viv"; +} from "@hms-dbmi/viv"; import { GLOBAL_SLIDER_DIMENSION_FIELDS } from "./constants"; import type { OME_TIFF, PixelSource } from "./state"; diff --git a/packages/vis/src/ImageView/index.tsx b/packages/vis/src/ImageView/index.tsx index bec9b29..3f27433 100644 --- a/packages/vis/src/ImageView/index.tsx +++ b/packages/vis/src/ImageView/index.tsx @@ -11,7 +11,7 @@ import { useChannelsStoreApi, DEFAULT_CHANNEL_STATE, } from './avivatorish/state'; -import { DetailView, VivViewer, getDefaultInitialViewState } from '@vivjs-experimental/viv'; +import { DetailView, VivViewer, getDefaultInitialViewState } from '@hms-dbmi/viv'; import { useImage } from './avivatorish/hooks'; /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc4cfc8..bfe6a79 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,9 @@ settings: catalogs: default: + '@hms-dbmi/viv': + specifier: ^0.19.0 + version: 0.19.0 '@luma.gl/core': specifier: ~9.1.9 version: 9.1.10 @@ -24,9 +27,6 @@ catalogs: '@vitejs/plugin-react': specifier: ^4.3.4 version: 4.7.0 - '@vivjs-experimental/viv': - specifier: ^1.0.1 - version: 1.0.1 anndata.js: specifier: ^0.0.2 version: 0.0.2 @@ -230,6 +230,9 @@ importers: packages/vis: dependencies: + '@hms-dbmi/viv': + specifier: 'catalog:' + version: 0.19.0(9a614091add8d9561d5b393ea4eef7a1) '@luma.gl/core': specifier: 'catalog:' version: 9.1.10 @@ -248,9 +251,6 @@ importers: '@uiw/react-json-view': specifier: 2.0.0-alpha.39 version: 2.0.0-alpha.39(@babel/runtime@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@vivjs-experimental/viv': - specifier: 'catalog:' - version: 1.0.1(9a614091add8d9561d5b393ea4eef7a1) anndata.js: specifier: 'catalog:' version: 0.0.2 @@ -1840,6 +1840,9 @@ packages: '@hapi/topo@5.1.0': resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} + '@hms-dbmi/viv@0.19.0': + resolution: {integrity: sha512-wH/GBjti9686xwuGnZp9sMwwkE6pLGRaRW3Qc27S/3GRwq5j6C9Fd3du12F4InFfle47GV2nv0X9mWO+wiRwsQ==} + '@interactjs/types@1.10.27': resolution: {integrity: sha512-BUdv0cvs4H5ODuwft2Xp4eL8Vmi3LcihK42z0Ft/FbVJZoRioBsxH+LlsBdK4tAie7PqlKGy+1oyOncu1nQ6eA==} @@ -2913,16 +2916,16 @@ packages: '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} - '@vivjs-experimental/constants@1.0.1': - resolution: {integrity: sha512-fVsQEMPAIL87AwPsWSD2KQVX1oHBCHgKh76+Q49pZ+d22viCD2qKBA9I86py4f5wcsTWaFc0mzH/TUXhjRXzDA==} + '@vivjs/constants@0.19.0': + resolution: {integrity: sha512-1X0cKfPGlFNscEgtXBhHsZu7BixMIq2cznZ1hNuvTnCtG7SDW9nsiAa3WrOMowDwYyzMFWkoaWmVcoJSdN0wkg==} - '@vivjs-experimental/extensions@1.0.1': - resolution: {integrity: sha512-2/FxSjRgAXve3qT54d96tmioCgPTmuKv0Ze4qKv+N+vwpZJzjCQJ2r8CEHjop1w+fvTtHq73361VCQdYePNYgw==} + '@vivjs/extensions@0.19.0': + resolution: {integrity: sha512-EHg7Y83+uv720micAaRjBh/Edw9lOq/uAfSQCbK0H4HBv7xUGzeXbCLmN/9ye8UgHWZd1cO6xvGDwBTQ7hp3gg==} peerDependencies: '@deck.gl/core': ~9.1.11 - '@vivjs-experimental/layers@1.0.1': - resolution: {integrity: sha512-20zykmWCaiF0AJywOflAZUaMAeGmuLBS0gJUnqnht/CKiv9NYGaP1VvwzkUdSRMl7ytqmXmkcAmMUt1Kv6lNbQ==} + '@vivjs/layers@0.19.0': + resolution: {integrity: sha512-M8zdlPsO8zEpESDevBiAnuEg9kv+H25B38XLfpPzTZS78hA6VD4PRw08xh5GjRHlaCV6CVnnKYAQDrvYNufDkg==} peerDependencies: '@deck.gl/core': ~9.1.11 '@deck.gl/geo-layers': ~9.1.11 @@ -2933,27 +2936,24 @@ packages: '@luma.gl/shadertools': ~9.1.9 '@luma.gl/webgl': ~9.1.9 - '@vivjs-experimental/loaders@1.0.1': - resolution: {integrity: sha512-7YLuKsc2fHxGSrYN3oI9unOzHQE8bc6175EKkdTKl/IbkZQOJ+RQtHpn13yQH35vHa46Ib0VhrioU36apAm/aA==} + '@vivjs/loaders@0.19.0': + resolution: {integrity: sha512-mCE2F8dih42hmIj96qYCPfvUqyNtuYlDKJa/i0LfT3Hrb5SclqgOpUrp4KW+C5Wy68O0P9c0ZIlmvWX0sMGJ8Q==} - '@vivjs-experimental/types@1.0.1': - resolution: {integrity: sha512-99qHIDP6o2FwkSeV9YB0ajO89ph9pP5Us+2Ks2jeC8stzjoXwnXATxuNX7r67/nu+tCSifzjgoSSrRnl2Zw1uQ==} + '@vivjs/types@0.19.0': + resolution: {integrity: sha512-936eBNXNmgSJUsfQ+iJcrC4oTVJS9vCb6J6I+TiSn7bymO0uKia+3yvLFN6cWGrjQiWo5hSTSh6ZV+ydiE4i5g==} - '@vivjs-experimental/viewers@1.0.1': - resolution: {integrity: sha512-vxqvTZ6kY2uGwXGuHckHBKbJXOKgP59tiqqDJ8BrIWpWy90wkYIyKTiwSc6o2sM4SzR6dB5QjzqELWspoH5c9w==} + '@vivjs/viewers@0.19.0': + resolution: {integrity: sha512-Y58b/4F9J3RjZjPG80GciikiWZHFyIncRHFsmVqpIdE/7ubTUJTvzI0ZdrT5Y6NoP7dxjevIrhfSWQGbmjgZJQ==} peerDependencies: '@deck.gl/react': ~9.1.11 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@vivjs-experimental/views@1.0.1': - resolution: {integrity: sha512-7m0F0SFbAm7HfzJCn9ofybIcRCNkJSHVVXb3upDiDxiWOD6PXPvKEpUWMb/kzxi1waaqZ9oM+mR1NBwBda2duA==} + '@vivjs/views@0.19.0': + resolution: {integrity: sha512-2jSH6mfHaFk1ovE5oFvS3fAdUaVqZGr14gY+HlEF7V4lASgALcJoVvA8oVJflHvUZIn8jiwOqfIcoF9t7/m5eg==} peerDependencies: '@deck.gl/core': ~9.1.11 '@deck.gl/layers': ~9.1.11 - '@vivjs-experimental/viv@1.0.1': - resolution: {integrity: sha512-HJAhRaD1tJiHQv6JRLENaPGFy66v7uOTh+TpV7+tsjcPi1FgsFnyzxBhdaHJUXYwi0em/aZO/r1LGdTa2zs/GQ==} - '@volar/language-core@2.4.23': resolution: {integrity: sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==} @@ -4280,9 +4280,6 @@ packages: eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} - eventemitter3@5.0.1: - resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} - events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -4950,6 +4947,7 @@ packages: jpeg-exif@1.1.4: resolution: {integrity: sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -5605,10 +5603,6 @@ packages: resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} engines: {node: '>=8'} - p-queue@7.4.1: - resolution: {integrity: sha512-vRpMXmIkYF2/1hLBKisKeVYJZ8S2tZ0zEAmIJgdVKP2nq0nh4qCdf8bgw+ZgKrkh71AOCaqzwbJJk1WtdcF3VA==} - engines: {node: '>=12'} - p-retry@6.2.1: resolution: {integrity: sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==} engines: {node: '>=16.17'} @@ -5617,10 +5611,6 @@ packages: resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} engines: {node: '>=8'} - p-timeout@5.1.0: - resolution: {integrity: sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew==} - engines: {node: '>=12'} - package-json@8.1.1: resolution: {integrity: sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==} engines: {node: '>=14.16'} @@ -7336,10 +7326,6 @@ packages: resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} engines: {node: '>=12.20'} - zarr@0.6.3: - resolution: {integrity: sha512-v7g3i/NfLEHtGtCEX8zl9b/LMY+8BY7fIYvbNX3nskAhliMCY5mA12jlc8Rbe91hSwL/4Nh2d3fUcVmnthXQkQ==} - engines: {node: '>=12'} - zarrita@0.5.1: resolution: {integrity: sha512-cyujP70BOl5DiXuLtM+0j9nq/pAov4SKXRYIQQOVnk2TfBg/jopX+FXLbqkq3ULOxFLB5AwkPbSp5KvZXoJrbQ==} @@ -9767,6 +9753,27 @@ snapshots: dependencies: '@hapi/hoek': 9.3.0 + '@hms-dbmi/viv@0.19.0(9a614091add8d9561d5b393ea4eef7a1)': + dependencies: + '@vivjs/constants': 0.19.0 + '@vivjs/extensions': 0.19.0(@deck.gl/core@9.1.15) + '@vivjs/layers': 0.19.0(95853861a3a9bfb6cf4e0bb4f1c1545a) + '@vivjs/loaders': 0.19.0 + '@vivjs/types': 0.19.0 + '@vivjs/viewers': 0.19.0(9a614091add8d9561d5b393ea4eef7a1) + '@vivjs/views': 0.19.0(95853861a3a9bfb6cf4e0bb4f1c1545a) + transitivePeerDependencies: + - '@deck.gl/core' + - '@deck.gl/geo-layers' + - '@deck.gl/layers' + - '@deck.gl/react' + - '@luma.gl/constants' + - '@luma.gl/core' + - '@luma.gl/engine' + - '@luma.gl/shadertools' + - '@luma.gl/webgl' + - react + '@interactjs/types@1.10.27': {} '@isaacs/balanced-match@4.0.1': {} @@ -11171,16 +11178,16 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 - '@vivjs-experimental/constants@1.0.1': + '@vivjs/constants@0.19.0': dependencies: '@luma.gl/constants': 9.1.10 - '@vivjs-experimental/extensions@1.0.1(@deck.gl/core@9.1.15)': + '@vivjs/extensions@0.19.0(@deck.gl/core@9.1.15)': dependencies: '@deck.gl/core': 9.1.15 - '@vivjs-experimental/constants': 1.0.1 + '@vivjs/constants': 0.19.0 - '@vivjs-experimental/layers@1.0.1(95853861a3a9bfb6cf4e0bb4f1c1545a)': + '@vivjs/layers@0.19.0(95853861a3a9bfb6cf4e0bb4f1c1545a)': dependencies: '@deck.gl/core': 9.1.15 '@deck.gl/geo-layers': 9.1.15(@deck.gl/core@9.1.15)(@deck.gl/extensions@9.1.15(@deck.gl/core@9.1.15)(@luma.gl/core@9.1.10)(@luma.gl/engine@9.1.10(@luma.gl/core@9.1.10)(@luma.gl/shadertools@9.1.10(@luma.gl/core@9.1.10))))(@deck.gl/layers@9.1.15(@deck.gl/core@9.1.15)(@loaders.gl/core@4.3.4)(@luma.gl/core@9.1.10)(@luma.gl/engine@9.1.10(@luma.gl/core@9.1.10)(@luma.gl/shadertools@9.1.10(@luma.gl/core@9.1.10))))(@deck.gl/mesh-layers@9.1.15(@deck.gl/core@9.1.15)(@loaders.gl/core@4.3.4)(@luma.gl/core@9.1.10)(@luma.gl/engine@9.1.10(@luma.gl/core@9.1.10)(@luma.gl/shadertools@9.1.10(@luma.gl/core@9.1.10))))(@loaders.gl/core@4.3.4)(@luma.gl/core@9.1.10)(@luma.gl/engine@9.1.10(@luma.gl/core@9.1.10)(@luma.gl/shadertools@9.1.10(@luma.gl/core@9.1.10))) @@ -11192,31 +11199,31 @@ snapshots: '@luma.gl/webgl': 9.1.10(@luma.gl/core@9.1.10) '@math.gl/core': 4.1.0 '@math.gl/culling': 4.1.0 - '@vivjs-experimental/constants': 1.0.1 - '@vivjs-experimental/extensions': 1.0.1(@deck.gl/core@9.1.15) - '@vivjs-experimental/loaders': 1.0.1 - '@vivjs-experimental/types': 1.0.1 + '@vivjs/constants': 0.19.0 + '@vivjs/extensions': 0.19.0(@deck.gl/core@9.1.15) + '@vivjs/loaders': 0.19.0 + '@vivjs/types': 0.19.0 - '@vivjs-experimental/loaders@1.0.1': + '@vivjs/loaders@0.19.0': dependencies: - '@vivjs-experimental/types': 1.0.1 + '@vivjs/types': 0.19.0 geotiff: 2.1.3 lzw-tiff-decoder: 0.1.1 quickselect: 2.0.0 - zarr: 0.6.3 + zarrita: 0.5.4 zod: 3.25.76 - '@vivjs-experimental/types@1.0.1': + '@vivjs/types@0.19.0': dependencies: - '@vivjs-experimental/constants': 1.0.1 + '@vivjs/constants': 0.19.0 math.gl: 4.1.0 - '@vivjs-experimental/viewers@1.0.1(9a614091add8d9561d5b393ea4eef7a1)': + '@vivjs/viewers@0.19.0(9a614091add8d9561d5b393ea4eef7a1)': dependencies: '@deck.gl/react': 9.1.15(@deck.gl/core@9.1.15)(@deck.gl/widgets@9.1.15(@deck.gl/core@9.1.15))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@vivjs-experimental/constants': 1.0.1 - '@vivjs-experimental/extensions': 1.0.1(@deck.gl/core@9.1.15) - '@vivjs-experimental/views': 1.0.1(95853861a3a9bfb6cf4e0bb4f1c1545a) + '@vivjs/constants': 0.19.0 + '@vivjs/extensions': 0.19.0(@deck.gl/core@9.1.15) + '@vivjs/views': 0.19.0(95853861a3a9bfb6cf4e0bb4f1c1545a) fast-deep-equal: 3.1.3 react: 19.2.0 transitivePeerDependencies: @@ -11229,13 +11236,13 @@ snapshots: - '@luma.gl/shadertools' - '@luma.gl/webgl' - '@vivjs-experimental/views@1.0.1(95853861a3a9bfb6cf4e0bb4f1c1545a)': + '@vivjs/views@0.19.0(95853861a3a9bfb6cf4e0bb4f1c1545a)': dependencies: '@deck.gl/core': 9.1.15 '@deck.gl/layers': 9.1.15(@deck.gl/core@9.1.15)(@loaders.gl/core@4.3.4)(@luma.gl/core@9.1.10)(@luma.gl/engine@9.1.10(@luma.gl/core@9.1.10)(@luma.gl/shadertools@9.1.10(@luma.gl/core@9.1.10))) '@math.gl/core': 4.1.0 - '@vivjs-experimental/layers': 1.0.1(95853861a3a9bfb6cf4e0bb4f1c1545a) - '@vivjs-experimental/loaders': 1.0.1 + '@vivjs/layers': 0.19.0(95853861a3a9bfb6cf4e0bb4f1c1545a) + '@vivjs/loaders': 0.19.0 math.gl: 4.1.0 transitivePeerDependencies: - '@deck.gl/geo-layers' @@ -11245,27 +11252,6 @@ snapshots: - '@luma.gl/shadertools' - '@luma.gl/webgl' - '@vivjs-experimental/viv@1.0.1(9a614091add8d9561d5b393ea4eef7a1)': - dependencies: - '@vivjs-experimental/constants': 1.0.1 - '@vivjs-experimental/extensions': 1.0.1(@deck.gl/core@9.1.15) - '@vivjs-experimental/layers': 1.0.1(95853861a3a9bfb6cf4e0bb4f1c1545a) - '@vivjs-experimental/loaders': 1.0.1 - '@vivjs-experimental/types': 1.0.1 - '@vivjs-experimental/viewers': 1.0.1(9a614091add8d9561d5b393ea4eef7a1) - '@vivjs-experimental/views': 1.0.1(95853861a3a9bfb6cf4e0bb4f1c1545a) - transitivePeerDependencies: - - '@deck.gl/core' - - '@deck.gl/geo-layers' - - '@deck.gl/layers' - - '@deck.gl/react' - - '@luma.gl/constants' - - '@luma.gl/core' - - '@luma.gl/engine' - - '@luma.gl/shadertools' - - '@luma.gl/webgl' - - react - '@volar/language-core@2.4.23': dependencies: '@volar/source-map': 2.4.23 @@ -12718,8 +12704,6 @@ snapshots: eventemitter3@4.0.7: {} - eventemitter3@5.0.1: {} - events@3.3.0: {} eventsource-parser@3.0.6: {} @@ -14370,11 +14354,6 @@ snapshots: eventemitter3: 4.0.7 p-timeout: 3.2.0 - p-queue@7.4.1: - dependencies: - eventemitter3: 5.0.1 - p-timeout: 5.1.0 - p-retry@6.2.1: dependencies: '@types/retry': 0.12.2 @@ -14385,8 +14364,6 @@ snapshots: dependencies: p-finally: 1.0.0 - p-timeout@5.1.0: {} - package-json@8.1.1: dependencies: got: 12.6.1 @@ -16312,11 +16289,6 @@ snapshots: yocto-queue@1.2.1: {} - zarr@0.6.3: - dependencies: - numcodecs: 0.2.2 - p-queue: 7.4.1 - zarrita@0.5.1: dependencies: '@zarrita/storage': 0.1.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1506479..becb2eb 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -9,7 +9,7 @@ catalog: '@types/react': ^19.2.0 '@types/react-dom': ^19.2.0 '@vitejs/plugin-react': ^4.3.4 - '@vivjs-experimental/viv': ^1.0.1 + '@hms-dbmi/viv': ^0.19.0 anndata.js: ^0.0.2 apache-arrow: ^17.0.0 deck.gl: ~9.1.11 From ec74e99e2bb7366a427d4216612fa65faadd9cc0 Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Fri, 30 Jan 2026 14:46:42 +0000 Subject: [PATCH 26/50] revert to vitessce cdn for parquet-wasm for time-being because of dev server issue #12 --- packages/core/src/models/VTableSource.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/core/src/models/VTableSource.ts b/packages/core/src/models/VTableSource.ts index 8066d9e..5e4b5a8 100644 --- a/packages/core/src/models/VTableSource.ts +++ b/packages/core/src/models/VTableSource.ts @@ -16,10 +16,22 @@ async function getParquetModule() { // that needs to be initialized before use in browser environments. // In Node.js, the module loads WASM synchronously so no init is needed. // (^^^ but why would it be invoked in node.js in the current version?) - const module = await import('parquet-wasm'); - if (typeof module.default === 'function') { - await module.default(); - } + //!!!!! broken in vite dev server, but not in build? + //we need an easy non-dev build to verify + //but we also need to fix this... + //- probably ultimately may be using geoarrow-wasm / investigate deck.gl arrow layer + // think about how that fits our 'core' (no deck deps) vs 'vis' structure etc. + // + // const module = await import('parquet-wasm'); + // if (typeof module.default === 'function') { + // await module.default(); + // } + + // Reference: https://observablehq.com/@kylebarron/geoparquet-on-the-web + // TODO: host somewhere we control, like cdn.vitessce.io? + // @ts-ignore + const module = await import(/* webpackIgnore: true */ 'https://cdn.vitessce.io/parquet-wasm@2c23652/esm/parquet_wasm.js'); + await module.default(); return { readParquet: module.readParquet, readSchema: module.readSchema }; } From 166c61189e50cfe86f1923b6aa83d7430a3cbda0 Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Fri, 30 Jan 2026 14:53:30 +0000 Subject: [PATCH 27/50] remove spatialdata-integrity --- python/spatialdata-integrity/.gitignore | 43 -- python/spatialdata-integrity/README.md | 96 ---- python/spatialdata-integrity/example_usage.py | 166 ------ python/spatialdata-integrity/pyproject.toml | 28 - .../src/spatialdata_integrity/__init__.py | 21 - .../src/spatialdata_integrity/checker.py | 541 ------------------ .../src/spatialdata_integrity/cli.py | 145 ----- .../spatialdata-integrity/test_known_good.py | 67 --- 8 files changed, 1107 deletions(-) delete mode 100644 python/spatialdata-integrity/.gitignore delete mode 100644 python/spatialdata-integrity/README.md delete mode 100644 python/spatialdata-integrity/example_usage.py delete mode 100644 python/spatialdata-integrity/pyproject.toml delete mode 100644 python/spatialdata-integrity/src/spatialdata_integrity/__init__.py delete mode 100644 python/spatialdata-integrity/src/spatialdata_integrity/checker.py delete mode 100644 python/spatialdata-integrity/src/spatialdata_integrity/cli.py delete mode 100644 python/spatialdata-integrity/test_known_good.py diff --git a/python/spatialdata-integrity/.gitignore b/python/spatialdata-integrity/.gitignore deleted file mode 100644 index bbca041..0000000 --- a/python/spatialdata-integrity/.gitignore +++ /dev/null @@ -1,43 +0,0 @@ -# Python -__pycache__/ -*.py[cod] -*$py.class -*.so -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg -*.egg - -# Virtual environments -.venv/ -venv/ -ENV/ -env/ - -# IDE -.vscode/ -.idea/ -*.swp -*.swo - -# Testing -.pytest_cache/ -.coverage -htmlcov/ - -# OS -.DS_Store -Thumbs.db - diff --git a/python/spatialdata-integrity/README.md b/python/spatialdata-integrity/README.md deleted file mode 100644 index b9e6315..0000000 --- a/python/spatialdata-integrity/README.md +++ /dev/null @@ -1,96 +0,0 @@ -# spatialdata-integrity - -Utilities for checking the integrity of SpatialData Zarr stores, detecting corrupt chunks, and validating data completeness. - -This is - as of now - a somewhat throwaway AI generated and untested set of scripts that might be useful later. I'm not sure if they should live here, but I wanted something I could potentially get back to later, and possibly publish to pypi if it's turned into anything more substantial. - -During development and testing of various permutations of SpatialData object and library versions, at some point I had an error saving an object which at first I wrongly attributed to a `spatialdata` bug - but appears to have been because I had some partial copy of the object I was working on. - - -## Features - -- **Zarr Array Integrity Checking**: Verify that all chunks in zarr arrays can be decompressed and read -- **Blosc Decompression Validation**: Detect corrupt blosc-compressed chunks -- **SpatialData Object Validation**: Check all elements (images, labels, points, shapes, tables) in a SpatialData object -- **Progress Reporting**: Detailed reporting of which elements/chunks are problematic -- **CLI Tool**: Easy-to-use command-line interface - -## Installation - -The package can be installed in development mode for use in both SpatialData.js and MDV projects: - -```bash -# Using uv (recommended) -cd python/spatialdata-integrity -uv pip install -e . - -# Or using pip -cd python/spatialdata-integrity -pip install -e . -``` - -For use in a specific project's environment (e.g., SpatialData.ts v0.6.1): - -```bash -# Install in SpatialData.ts v0.6.1 environment -cd python/v0.6.1 -uv pip install -e ../spatialdata-integrity - -# Or in MDV -cd python -pip install -e ../SpatialData.ts/python/spatialdata-integrity -``` - -## Usage - -### CLI - -```bash -# Check a SpatialData zarr store -check-spatialdata /path/to/spatialdata.zarr - -# Verbose output -check-spatialdata /path/to/spatialdata.zarr --verbose - -# Check only specific element types -check-spatialdata /path/to/spatialdata.zarr --elements images labels - -# Output results to JSON -check-spatialdata /path/to/spatialdata.zarr --output results.json -``` - -### Python API - -```python -from spatialdata_integrity import check_spatialdata, check_zarr_array -import spatialdata as sd - -# Check a full SpatialData object -sdata = sd.read_zarr("path/to/spatialdata.zarr") -results = check_spatialdata(sdata) -if results.is_valid: - print("All checks passed!") -else: - print(f"Found {len(results.errors)} errors") - for error in results.errors: - print(f" - {error}") - -# Check a specific zarr array -import zarr -arr = zarr.open("path/to/array.zarr") -results = check_zarr_array(arr) -``` - -## Example Output - -``` -Checking SpatialData object: /path/to/spatialdata.zarr -✓ Images: 'image1' (3 chunks checked) -✓ Labels: 'labels1' (12 chunks checked) -✗ Images: 'image2' - Blosc decompression error at chunk (0, 0, 0) -✓ Points: 'points1' (1 chunk checked) -✓ Tables: 'table1' (validated) - -Summary: 1 error found in 1 element -``` - diff --git a/python/spatialdata-integrity/example_usage.py b/python/spatialdata-integrity/example_usage.py deleted file mode 100644 index 2ca539f..0000000 --- a/python/spatialdata-integrity/example_usage.py +++ /dev/null @@ -1,166 +0,0 @@ -#!/usr/bin/env python3 -""" -Example usage of spatialdata-integrity checker. - -This demonstrates how to use the integrity checker both programmatically -and via the CLI. -""" - -import sys -from pathlib import Path - -# Add src to path for development -sys.path.insert(0, str(Path(__file__).parent / "src")) - -from spatialdata_integrity import check_spatialdata, check_zarr_array -import spatialdata as sd - - -def example_check_spatialdata(): - """Example: Check a full SpatialData object.""" - print("=" * 60) - print("Example 1: Checking a SpatialData object") - print("=" * 60) - - # Replace with your path - path = "path/to/your/spatialdata.zarr" - - if not Path(path).exists(): - print(f"Path does not exist: {path}") - print("Please update the path in this script.") - return - - try: - result = check_spatialdata(path, verbose=True) - print("\n" + str(result)) - - if result.is_valid: - print("\n✓ All checks passed!") - else: - print("\n✗ Found errors:") - for element in result.elements: - if not element.is_valid: - print(f" - {element.element_type} '{element.element_name}':") - for error in element.errors: - print(f" * Chunk {error.chunk_index}: {error.error_type}") - print(f" {error.error_message}") - except Exception as e: - print(f"Error: {e}") - import traceback - traceback.print_exc() - - -def example_check_specific_elements(): - """Example: Check only specific element types.""" - print("\n" + "=" * 60) - print("Example 2: Checking only images and labels") - print("=" * 60) - - path = "path/to/your/spatialdata.zarr" - - if not Path(path).exists(): - print(f"Path does not exist: {path}") - return - - try: - # Check only images and labels - result = check_spatialdata( - path, - element_types=["images", "labels"], - verbose=True - ) - print("\n" + str(result)) - except Exception as e: - print(f"Error: {e}") - - -def example_check_zarr_array(): - """Example: Check a specific zarr array.""" - print("\n" + "=" * 60) - print("Example 3: Checking a specific zarr array") - print("=" * 60) - - import zarr - - # Open a zarr array directly - array_path = "path/to/array.zarr" - - if not Path(array_path).exists(): - print(f"Path does not exist: {array_path}") - return - - try: - arr = zarr.open(array_path, mode="r") - result = check_zarr_array(arr, array_path=array_path) - - print(f"\nArray: {array_path}") - print(f"Shape: {arr.shape}") - print(f"Chunks: {arr.chunks}") - print(f"Chunks checked: {result.chunks_checked}") - - if result.is_valid: - print("✓ All chunks are valid") - else: - print("✗ Found errors:") - for error in result.errors: - print(f" - Chunk {error.chunk_index}: {error.error_type}") - print(f" {error.error_message}") - except Exception as e: - print(f"Error: {e}") - - -def example_find_corrupt_chunks(): - """Example: Find which specific chunks are corrupt.""" - print("\n" + "=" * 60) - print("Example 4: Finding corrupt chunks") - print("=" * 60) - - path = "path/to/your/spatialdata.zarr" - - if not Path(path).exists(): - print(f"Path does not exist: {path}") - return - - try: - result = check_spatialdata(path) - - # Find all corrupt chunks - corrupt_chunks = [] - for element in result.elements: - if not element.is_valid: - for error in element.errors: - corrupt_chunks.append({ - "element": f"{element.element_type}/{element.element_name}", - "chunk": error.chunk_index, - "error": error.error_type, - "message": error.error_message, - }) - - if corrupt_chunks: - print(f"\nFound {len(corrupt_chunks)} corrupt chunk(s):") - for chunk_info in corrupt_chunks: - print(f"\n Element: {chunk_info['element']}") - print(f" Chunk index: {chunk_info['chunk']}") - print(f" Error type: {chunk_info['error']}") - print(f" Message: {chunk_info['message']}") - else: - print("\n✓ No corrupt chunks found") - - except Exception as e: - print(f"Error: {e}") - - -if __name__ == "__main__": - print("SpatialData Integrity Checker - Usage Examples") - print("=" * 60) - print("\nNote: Update the paths in this script to point to your data.") - print("\nUncomment the example you want to run:\n") - - # Uncomment the example you want to run: - # example_check_spatialdata() - # example_check_specific_elements() - # example_check_zarr_array() - # example_find_corrupt_chunks() - - print("\nTo run examples, uncomment them in the script.") - diff --git a/python/spatialdata-integrity/pyproject.toml b/python/spatialdata-integrity/pyproject.toml deleted file mode 100644 index c65e420..0000000 --- a/python/spatialdata-integrity/pyproject.toml +++ /dev/null @@ -1,28 +0,0 @@ -[project] -name = "spatialdata-integrity" -version = "0.1.0" -description = "Utilities for checking integrity of SpatialData Zarr stores" -readme = "README.md" -requires-python = ">=3.10" -dependencies = [ - "zarr>=3.0.0", - "spatialdata>=0.5.0", - "numpy>=1.20.0", -] - -[project.optional-dependencies] -dev = [ - "pytest>=7.0.0", - "pytest-asyncio>=0.21.0", -] - -[project.scripts] -check-spatialdata = "spatialdata_integrity.cli:main" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.hatch.build.targets.wheel] -packages = ["src/spatialdata_integrity"] - diff --git a/python/spatialdata-integrity/src/spatialdata_integrity/__init__.py b/python/spatialdata-integrity/src/spatialdata_integrity/__init__.py deleted file mode 100644 index 6d3eed3..0000000 --- a/python/spatialdata-integrity/src/spatialdata_integrity/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -""" -Utilities for checking integrity of SpatialData Zarr stores. -""" - -from .checker import ( - check_spatialdata, - check_zarr_array, - IntegrityResult, - ElementResult, - ChunkError, -) - -__version__ = "0.1.0" -__all__ = [ - "check_spatialdata", - "check_zarr_array", - "IntegrityResult", - "ElementResult", - "ChunkError", -] - diff --git a/python/spatialdata-integrity/src/spatialdata_integrity/checker.py b/python/spatialdata-integrity/src/spatialdata_integrity/checker.py deleted file mode 100644 index e1c74d8..0000000 --- a/python/spatialdata-integrity/src/spatialdata_integrity/checker.py +++ /dev/null @@ -1,541 +0,0 @@ -""" -Core integrity checking functions for SpatialData and Zarr arrays. -""" - -from dataclasses import dataclass, field -from pathlib import Path -from typing import Optional, Union, List, Dict, Any -import traceback - -try: - import zarr - import numpy as np - import spatialdata as sd -except ImportError as e: - raise ImportError( - "spatialdata-integrity requires zarr, numpy, and spatialdata. " - f"Install them with: pip install zarr numpy spatialdata\n" - f"Original error: {e}" - ) - - -@dataclass -class ChunkError: - """Information about a corrupt chunk.""" - - chunk_index: tuple - error_type: str - error_message: str - array_path: Optional[str] = None - - -@dataclass -class ElementResult: - """Result of checking a single SpatialData element.""" - - element_type: str # 'images', 'labels', 'points', 'shapes', 'tables' - element_name: str - is_valid: bool - chunks_checked: int = 0 - errors: List[ChunkError] = field(default_factory=list) - warning: Optional[str] = None - - def __str__(self) -> str: - status = "✓" if self.is_valid else "✗" - msg = f"{status} {self.element_type.capitalize()}: '{self.element_name}'" - if self.chunks_checked > 0: - msg += f" ({self.chunks_checked} chunks checked)" - if self.warning: - msg += f" - Warning: {self.warning}" - if self.errors: - for error in self.errors: - msg += f"\n - Error at chunk {error.chunk_index}: {error.error_type}" - return msg - - -@dataclass -class IntegrityResult: - """Result of checking a SpatialData object.""" - - path: Optional[str] = None - is_valid: bool = True - elements: List[ElementResult] = field(default_factory=list) - errors: List[str] = field(default_factory=list) - - def __str__(self) -> str: - lines = [] - if self.path: - lines.append(f"Checking SpatialData object: {self.path}") - else: - lines.append("Checking SpatialData object") - - for element in self.elements: - lines.append(f" {element}") - - if self.errors: - lines.append("") - lines.append("Errors encountered:") - for error in self.errors: - lines.append(f" - {error}") - - lines.append("") - error_count = sum(1 for e in self.elements if not e.is_valid) - lines.append(f"Summary: {error_count} error(s) found in {len(self.elements)} element(s)") - - return "\n".join(lines) - - -def check_zarr_array( - array: zarr.Array, - sample_chunks: bool = True, - max_chunks_to_check: Optional[int] = None, - array_path: Optional[str] = None, -) -> ElementResult: - """ - Check the integrity of a zarr array by attempting to read chunks. - - Args: - array: The zarr array to check - sample_chunks: If True, sample chunks across the array. If False, check all chunks. - max_chunks_to_check: Maximum number of chunks to check (None = check all) - array_path: Optional path identifier for error reporting - - Returns: - ElementResult with validation results - """ - errors: List[ChunkError] = [] - chunks_checked = 0 - - try: - # Get array shape and chunks - shape = array.shape - chunks = array.chunks - - if chunks is None: - # No chunking, try to read a small slice - try: - # Read a small sample from the beginning - sample_shape = tuple(min(10, s) for s in shape) - _ = array[tuple(slice(0, s) for s in sample_shape)] - chunks_checked = 1 - except Exception as e: - error_type = type(e).__name__ - error_message = str(e) - errors.append( - ChunkError( - chunk_index=(0,) * len(shape), - error_type=error_type, - error_message=error_message, - array_path=array_path, - ) - ) - chunks_checked = 1 - else: - # Calculate chunk indices - chunk_indices = [] - for dim_size, chunk_size in zip(shape, chunks): - num_chunks = (dim_size + chunk_size - 1) // chunk_size - chunk_indices.append(list(range(num_chunks))) - - # Generate chunk coordinates to check - import itertools - - all_chunk_coords = list(itertools.product(*chunk_indices)) - - if sample_chunks and len(all_chunk_coords) > 10: - # Sample chunks: first, last, and a few in between - import random - - samples = [all_chunk_coords[0], all_chunk_coords[-1]] - if len(all_chunk_coords) > 2: - samples.extend( - random.sample( - all_chunk_coords[1:-1], - min(8, len(all_chunk_coords) - 2), - ) - ) - chunks_to_check = samples - else: - chunks_to_check = all_chunk_coords - - # Limit number of chunks if specified - if max_chunks_to_check is not None: - chunks_to_check = chunks_to_check[:max_chunks_to_check] - - # Check each chunk - for chunk_coords in chunks_to_check: - try: - # Calculate slice for this chunk - slices = [] - for coord, dim_size, chunk_size in zip(chunk_coords, shape, chunks): - start = coord * chunk_size - end = min(start + chunk_size, dim_size) - slices.append(slice(start, end)) - - # Try to read the chunk - _ = array[tuple(slices)] - chunks_checked += 1 - except Exception as e: - error_type = type(e).__name__ - error_message = str(e) - - # Check if it's a blosc decompression error - if "blosc" in error_message.lower() or "decompression" in error_message.lower(): - error_type = "BloscDecompressionError" - - errors.append( - ChunkError( - chunk_index=chunk_coords, - error_type=error_type, - error_message=error_message, - array_path=array_path, - ) - ) - chunks_checked += 1 - - except Exception as e: - # Error accessing array metadata - error_type = type(e).__name__ - error_message = str(e) - errors.append( - ChunkError( - chunk_index=(), - error_type=error_type, - error_message=error_message, - array_path=array_path, - ) - ) - - return ElementResult( - element_type="array", - element_name=array_path or "unknown", - is_valid=len(errors) == 0, - chunks_checked=chunks_checked, - errors=errors, - ) - - -def check_spatialdata_element( - element: Any, - element_type: str, - element_name: str, - verbose: bool = False, -) -> ElementResult: - """ - Check a single SpatialData element (image, label, points, shape, or table). - - Args: - element: The SpatialData element to check - element_type: Type of element ('images', 'labels', 'points', 'shapes', 'tables') - element_name: Name of the element - verbose: If True, provide more detailed output - - Returns: - ElementResult with validation results - """ - errors: List[ChunkError] = [] - chunks_checked = 0 - warning: Optional[str] = None - - try: - if element_type in ("images", "labels"): - # Images and labels are typically DataTree or similar with .data attribute - if hasattr(element, "data"): - data = element.data - - # Try to access as DataTree (multi-scale) - # DataTree objects can be iterated or accessed via .values() or items() - scale_levels = None - if hasattr(data, "values"): - # DataTree with .values() method - try: - scale_levels = list(data.values()) - except Exception: - pass - elif hasattr(data, "items"): - # DataTree with .items() method - try: - scale_levels = [(k, v) for k, v in data.items()] - except Exception: - pass - elif hasattr(data, "__iter__") and not isinstance(data, (str, bytes)): - # Try as iterable - try: - scale_levels = list(data) - except Exception: - pass - - if scale_levels: - # Multi-scale: check each scale level - for i, scale_item in enumerate(scale_levels): - if isinstance(scale_item, tuple): - scale_name, scale_data = scale_item - else: - scale_name = f"scale_{i}" - scale_data = scale_item - - # Try to get underlying zarr array from dask array - zarr_array = None - if hasattr(scale_data, "store"): - # Dask array with store attribute - try: - # Try to get zarr array from dask - if hasattr(scale_data.store, "array"): - zarr_array = scale_data.store.array - except Exception: - pass - - # If we have a zarr array, check it directly - if zarr_array is not None and hasattr(zarr_array, "chunks"): - result = check_zarr_array( - zarr_array, - array_path=f"{element_name}/{scale_name}", - ) - chunks_checked += result.chunks_checked - errors.extend(result.errors) - else: - # Try to read a small sample to trigger chunk access - try: - # Get shape and read a small slice - if hasattr(scale_data, "shape"): - shape = scale_data.shape - # Read first chunk or small slice - ndim = len(shape) - if ndim >= 2: - # Read a small 2D slice - slices = tuple(slice(0, min(10, s)) for s in shape[:2]) - if ndim > 2: - # For higher dims, take first index - slices = slices + tuple(0 for _ in range(ndim - 2)) - - # Try to access - this will trigger chunk loading - sample = scale_data[slices] - - # If it's a dask array, compute it - if hasattr(sample, "compute"): - _ = sample.compute() - - chunks_checked += 1 - else: - # 1D or 0D - just try to access - _ = scale_data[0] if shape[0] > 0 else scale_data - chunks_checked += 1 - else: - # No shape attribute - try basic access - _ = scale_data[0] if hasattr(scale_data, "__getitem__") else scale_data - chunks_checked += 1 - except Exception as e: - error_type = type(e).__name__ - error_message = str(e) - if "blosc" in error_message.lower() or "decompression" in error_message.lower(): - error_type = "BloscDecompressionError" - errors.append( - ChunkError( - chunk_index=(scale_name,), - error_type=error_type, - error_message=error_message, - array_path=f"{element_name}/{scale_name}", - ) - ) - chunks_checked += 1 - elif hasattr(data, "chunks"): - # Single zarr array (not multi-scale) - result = check_zarr_array(data, array_path=element_name) - chunks_checked += result.chunks_checked - errors.extend(result.errors) - else: - # Try to access directly as array-like - try: - if hasattr(data, "shape"): - shape = data.shape - if len(shape) >= 2: - slices = tuple(slice(0, min(10, s)) for s in shape[:2]) - sample = data[slices] - if hasattr(sample, "compute"): - _ = sample.compute() - chunks_checked += 1 - else: - _ = data[0] if shape[0] > 0 else data - chunks_checked += 1 - else: - warning = "Could not determine array structure - no shape attribute" - except Exception as e: - error_type = type(e).__name__ - error_message = str(e) - if "blosc" in error_message.lower(): - error_type = "BloscDecompressionError" - errors.append( - ChunkError( - chunk_index=(), - error_type=error_type, - error_message=error_message, - array_path=element_name, - ) - ) - chunks_checked += 1 - else: - warning = "Element has no 'data' attribute" - - elif element_type == "points": - # Points are typically dask DataFrames - # For now, just check if we can access metadata - try: - if hasattr(element, "compute"): - # Try a small sample - sample = element.head(10) - _ = sample.compute() if hasattr(sample, "compute") else sample - chunks_checked = 1 - else: - # Already computed or not dask - _ = element.head(10) - chunks_checked = 1 - except Exception as e: - error_type = type(e).__name__ - error_message = str(e) - errors.append( - ChunkError( - chunk_index=(), - error_type=error_type, - error_message=error_message, - array_path=element_name, - ) - ) - chunks_checked = 1 - - elif element_type == "shapes": - # Shapes are typically GeoDataFrames - # Just check if we can access them - try: - _ = len(element) - chunks_checked = 1 - except Exception as e: - error_type = type(e).__name__ - error_message = str(e) - errors.append( - ChunkError( - chunk_index=(), - error_type=error_type, - error_message=error_message, - array_path=element_name, - ) - ) - chunks_checked = 1 - - elif element_type == "tables": - # Tables are AnnData objects - try: - # Check if we can access the data - _ = element.shape - _ = element.X - chunks_checked = 1 - except Exception as e: - error_type = type(e).__name__ - error_message = str(e) - errors.append( - ChunkError( - chunk_index=(), - error_type=error_type, - error_message=error_message, - array_path=element_name, - ) - ) - chunks_checked = 1 - - except Exception as e: - error_type = type(e).__name__ - error_message = str(e) - errors.append( - ChunkError( - chunk_index=(), - error_type=error_type, - error_message=error_message, - array_path=element_name, - ) - ) - - return ElementResult( - element_type=element_type, - element_name=element_name, - is_valid=len(errors) == 0, - chunks_checked=chunks_checked, - errors=errors, - warning=warning, - ) - - -def check_spatialdata( - sdata: Union[sd.SpatialData, str, Path], - element_types: Optional[List[str]] = None, - verbose: bool = False, -) -> IntegrityResult: - """ - Check the integrity of a SpatialData object. - - Args: - sdata: SpatialData object or path to zarr store - element_types: Optional list of element types to check (e.g., ['images', 'labels']). - If None, checks all element types. - verbose: If True, provide more detailed output - - Returns: - IntegrityResult with validation results - """ - # Load SpatialData if path provided - if isinstance(sdata, (str, Path)): - path = str(sdata) - try: - sdata = sd.read_zarr(path) - except Exception as e: - return IntegrityResult( - path=path, - is_valid=False, - errors=[f"Failed to load SpatialData object: {type(e).__name__}: {e}"], - ) - else: - path = None - - if element_types is None: - element_types = ["images", "labels", "points", "shapes", "tables"] - - results: List[ElementResult] = [] - all_errors: List[str] = [] - - # Check each element type - for element_type in element_types: - if not hasattr(sdata, element_type): - continue - - elements = getattr(sdata, element_type) - if elements is None: - continue - - if isinstance(elements, dict): - # Multiple elements of this type - for element_name, element in elements.items(): - try: - result = check_spatialdata_element( - element, element_type, element_name, verbose=verbose - ) - results.append(result) - except Exception as e: - all_errors.append( - f"Error checking {element_type} '{element_name}': {type(e).__name__}: {e}" - ) - if verbose: - all_errors.append(traceback.format_exc()) - elif elements is not None: - # Single element (unlikely but possible) - result = check_spatialdata_element( - elements, element_type, element_type, verbose=verbose - ) - results.append(result) - - is_valid = len(all_errors) == 0 and all(r.is_valid for r in results) - - return IntegrityResult( - path=path, - is_valid=is_valid, - elements=results, - errors=all_errors, - ) - diff --git a/python/spatialdata-integrity/src/spatialdata_integrity/cli.py b/python/spatialdata-integrity/src/spatialdata_integrity/cli.py deleted file mode 100644 index 71e8ffe..0000000 --- a/python/spatialdata-integrity/src/spatialdata_integrity/cli.py +++ /dev/null @@ -1,145 +0,0 @@ -""" -Command-line interface for spatialdata-integrity. -""" - -import argparse -import json -import sys -from pathlib import Path -from typing import Optional - -from .checker import check_spatialdata, IntegrityResult - - -def format_json_result(result: IntegrityResult) -> str: - """Format result as JSON.""" - output = { - "path": result.path, - "is_valid": result.is_valid, - "elements": [ - { - "element_type": e.element_type, - "element_name": e.element_name, - "is_valid": e.is_valid, - "chunks_checked": e.chunks_checked, - "errors": [ - { - "chunk_index": list(e.chunk_index), - "error_type": e.error_type, - "error_message": e.error_message, - "array_path": e.array_path, - } - for e in element.errors - ], - "warning": e.warning, - } - for e in result.elements - ], - "errors": result.errors, - } - return json.dumps(output, indent=2) - - -def main(): - """Main CLI entry point.""" - parser = argparse.ArgumentParser( - description="Check integrity of SpatialData Zarr stores", - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=""" -Examples: - # Check a SpatialData store - check-spatialdata /path/to/spatialdata.zarr - - # Verbose output - check-spatialdata /path/to/spatialdata.zarr --verbose - - # Check only images and labels - check-spatialdata /path/to/spatialdata.zarr --elements images labels - - # Output to JSON file - check-spatialdata /path/to/spatialdata.zarr --output results.json - """, - ) - - parser.add_argument( - "path", - type=str, - help="Path to SpatialData Zarr store", - ) - - parser.add_argument( - "--elements", - nargs="+", - choices=["images", "labels", "points", "shapes", "tables"], - default=None, - help="Element types to check (default: all)", - ) - - parser.add_argument( - "--verbose", - "-v", - action="store_true", - help="Verbose output", - ) - - parser.add_argument( - "--output", - "-o", - type=str, - default=None, - help="Output file path (JSON format). If not specified, prints to stdout.", - ) - - parser.add_argument( - "--format", - choices=["text", "json"], - default="text", - help="Output format (default: text)", - ) - - args = parser.parse_args() - - # Check if path exists - path = Path(args.path) - if not path.exists(): - print(f"Error: Path does not exist: {path}", file=sys.stderr) - sys.exit(1) - - # Run integrity check - try: - result = check_spatialdata( - str(path), - element_types=args.elements, - verbose=args.verbose, - ) - except Exception as e: - print(f"Error: Failed to check SpatialData object: {e}", file=sys.stderr) - if args.verbose: - import traceback - - traceback.print_exc() - sys.exit(1) - - # Format output - if args.format == "json": - output = format_json_result(result) - else: - output = str(result) - - # Write output - if args.output: - output_path = Path(args.output) - output_path.parent.mkdir(parents=True, exist_ok=True) - output_path.write_text(output) - print(f"Results written to: {output_path}", file=sys.stderr) - else: - print(output) - - # Exit with error code if validation failed - if not result.is_valid: - sys.exit(1) - - -if __name__ == "__main__": - main() - diff --git a/python/spatialdata-integrity/test_known_good.py b/python/spatialdata-integrity/test_known_good.py deleted file mode 100644 index 3027d64..0000000 --- a/python/spatialdata-integrity/test_known_good.py +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env python3 -""" -Test script to verify integrity checking works with known-good data. - -This script tests the integrity checker with the blobs dataset, -which should be valid and not have any corrupt chunks. -""" - -import sys -from pathlib import Path - -# Add src to path for development -sys.path.insert(0, str(Path(__file__).parent / "src")) - -from spatialdata_integrity import check_spatialdata - - -def test_known_good(): - """Test with known-good blobs dataset.""" - # Try to find blobs dataset in test-fixtures - project_root = Path(__file__).parent.parent.parent - fixtures_dir = project_root / "test-fixtures" - - # Try v0.6.1 first, then v0.5.0 - test_paths = [ - fixtures_dir / "v0.6.1" / "blobs.zarr", - fixtures_dir / "v0.5.0" / "blobs.zarr", - ] - - test_path = None - for path in test_paths: - if path.exists(): - test_path = path - break - - if test_path is None: - print("Error: Could not find blobs.zarr in test-fixtures") - print(f"Looked in: {[str(p) for p in test_paths]}") - return 1 - - print(f"Testing with known-good dataset: {test_path}") - print("=" * 60) - - try: - result = check_spatialdata(str(test_path), verbose=True) - print("\n" + str(result)) - - if result.is_valid: - print("\n✓ All checks passed! The integrity checker is working correctly.") - return 0 - else: - print("\n✗ Found errors in known-good dataset. This may indicate:") - print(" 1. The dataset is actually corrupted") - print(" 2. There's a bug in the integrity checker") - return 1 - - except Exception as e: - print(f"\n✗ Error running integrity check: {e}") - import traceback - - traceback.print_exc() - return 1 - - -if __name__ == "__main__": - sys.exit(test_known_good()) - From f24a3acd225dd2b751e07edd1560a693af4e28b1 Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Fri, 30 Jan 2026 15:20:27 +0000 Subject: [PATCH 28/50] conditional use of parquet-wasm cdn so tests don't fail --- packages/core/src/models/VTableSource.ts | 42 +++++++++++++++--------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/packages/core/src/models/VTableSource.ts b/packages/core/src/models/VTableSource.ts index 5e4b5a8..8242ec7 100644 --- a/packages/core/src/models/VTableSource.ts +++ b/packages/core/src/models/VTableSource.ts @@ -1,5 +1,8 @@ // this is a direct copy of the Vitessce implementation, with changes mostly to make it more normal TypeScript. +// and this for condtional version of parquet-wasm... +/// + import { tableFromIPC, type Table as ArrowTable } from 'apache-arrow'; import type { DataSourceParams } from '../Vutils'; import AnnDataSource from './VAnnDataSource'; @@ -15,23 +18,30 @@ async function getParquetModule() { // Dynamic import for code-splitting. parquet-wasm is a WebAssembly module // that needs to be initialized before use in browser environments. // In Node.js, the module loads WASM synchronously so no init is needed. - // (^^^ but why would it be invoked in node.js in the current version?) - //!!!!! broken in vite dev server, but not in build? - //we need an easy non-dev build to verify - //but we also need to fix this... - //- probably ultimately may be using geoarrow-wasm / investigate deck.gl arrow layer - // think about how that fits our 'core' (no deck deps) vs 'vis' structure etc. // - // const module = await import('parquet-wasm'); - // if (typeof module.default === 'function') { - // await module.default(); - // } - - // Reference: https://observablehq.com/@kylebarron/geoparquet-on-the-web - // TODO: host somewhere we control, like cdn.vitessce.io? - // @ts-ignore - const module = await import(/* webpackIgnore: true */ 'https://cdn.vitessce.io/parquet-wasm@2c23652/esm/parquet_wasm.js'); - await module.default(); + // TODO: Replace with a more civilised parquet module that's built in a way we can actually consume. + // - probably ultimately may be using geoarrow-wasm / investigate deck.gl arrow layer + // think about how that fits our 'core' (no deck deps) vs 'vis' structure etc. + + // Check if we're in a browser/vite dev server environment + if (import.meta.env?.DEV) { + // Use CDN version in vite dev server (workaround for module loading issues) + // Reference: https://observablehq.com/@kylebarron/geoparquet-on-the-web + console.warn( + '[VTableSource] Using CDN version of parquet-wasm in vite dev server. ' + + 'This is a temporary workaround pending a better parquet module solution.' + ); + // @ts-ignore - CDN import not recognized by TypeScript + const module = await import(/* webpackIgnore: true */ 'https://cdn.vitessce.io/parquet-wasm@2c23652/esm/parquet_wasm.js'); + await module.default(); + return { readParquet: module.readParquet, readSchema: module.readSchema }; + } + + // Use package import in Node.js or production builds + const module = await import('parquet-wasm'); + if (typeof module.default === 'function') { + await module.default(); + } return { readParquet: module.readParquet, readSchema: module.readSchema }; } From e75239db0610f3cbf461d1a7dee394702cf1744a Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Fri, 30 Jan 2026 15:41:05 +0000 Subject: [PATCH 29/50] add new py spatialdata 0.7.0 to fixtures, be more consistent in ome vs spatialdata schema naming. --- README.md | 27 +- package.json | 2 + packages/core/src/schemas/index.ts | 25 +- python/scripts/generate_fixtures.py | 8 +- python/scripts/validate_datasets.py | 18 +- python/v0.6.1/generate_fixtures.py | 14 +- python/v0.7.0/generate_fixtures.py | 126 ++ python/v0.7.0/pyproject.toml | 9 + python/v0.7.0/uv.lock | 2102 +++++++++++++++++++++++++++ scripts/validate-all.sh | 8 +- scripts/validate-datasets-js.js | 8 +- tests/integration/fixtures.test.ts | 13 +- tests/unit/schemas.test.ts | 33 +- 13 files changed, 2335 insertions(+), 58 deletions(-) create mode 100755 python/v0.7.0/generate_fixtures.py create mode 100644 python/v0.7.0/pyproject.toml create mode 100644 python/v0.7.0/uv.lock diff --git a/README.md b/README.md index ff04aa1..f685894 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,10 @@ uv sync --directory python/v0.5.0 # Set up environment for spatialdata 0.6.1 uv sync --directory python/v0.6.1 -# Or set up both at once (the fixture generation script will do this automatically) +# Set up environment for spatialdata 0.7.0 +uv sync --directory python/v0.7.0 + +# Or set up all at once (the fixture generation script will do this automatically) ``` Each environment: @@ -96,6 +99,10 @@ Each environment: **Note:** The fixture generation script automatically sets up these environments when needed. You only need to manually sync if you want to activate an environment directly or use it in your editor. +**Version Mapping:** +- **spatialdata 0.5.0** uses **OME-NGFF 0.4** format (multiscales at top level) in **zarr v2** stores (consolidated `zmetadata`) +- **spatialdata 0.6.0+** uses **OME-NGFF 0.5** format (multiscales nested under `ome` key) in **zarr v3** stores (consolidated `zarr.json`) + #### Editor Setup The project includes editor configuration files (`.vscode/settings.json` and `.cursor/settings.json`) that configure the Python interpreter to use one of the version-specific environments (defaults to v0.6.1). @@ -105,11 +112,13 @@ The project includes editor configuration files (`.vscode/settings.json` and `.c - To switch versions: `Cmd/Ctrl+Shift+P` → "Python: Select Interpreter" → Choose: - `./python/v0.5.0/.venv/bin/python3` for spatialdata 0.5.0 - `./python/v0.6.1/.venv/bin/python3` for spatialdata 0.6.1 + - `./python/v0.7.0/.venv/bin/python3` for spatialdata 0.7.0 **Other editors:** - Choose the appropriate virtual environment: - `python/v0.5.0/.venv/bin/python3` for spatialdata 0.5.0 - `python/v0.6.1/.venv/bin/python3` for spatialdata 0.6.1 + - `python/v0.7.0/.venv/bin/python3` for spatialdata 0.7.0 ### Running Tests @@ -135,17 +144,18 @@ Test fixtures are generated on-demand using the Python `spatialdata` library. Ea Fixtures are stored in `test-fixtures/` (excluded from git). ```bash -# Generate fixtures for both spatialdata versions (0.5.0 and 0.6.1) +# Generate fixtures for all spatialdata versions (0.5.0, 0.6.1, and 0.7.0) # This will automatically set up the version-specific environments if needed pnpm test:fixtures:generate # Generate fixtures for a specific version pnpm test:fixtures:generate:0.5.0 pnpm test:fixtures:generate:0.6.1 +pnpm test:fixtures:generate:0.7.0 ``` **How it works:** -- The script uses separate environments: `python/v0.5.0/` and `python/v0.6.1/` +- The script uses separate environments: `python/v0.5.0/`, `python/v0.6.1/`, and `python/v0.7.0/` - Each environment has its own `pyproject.toml` with the spatialdata version pinned - The script automatically runs `uv sync` for each environment before generating fixtures - This ensures fixtures are generated with the exact spatialdata version being tested @@ -166,6 +176,7 @@ pnpm test:server Once running, fixtures are accessible at: - `http://localhost:8080/test-fixtures/v0.5.0/blobs.zarr` - `http://localhost:8080/test-fixtures/v0.6.1/blobs.zarr` +- `http://localhost:8080/test-fixtures/v0.7.0/blobs.zarr` The server provides directory listings and serves all zarr metadata files with appropriate CORS headers. @@ -203,19 +214,23 @@ const sdata = await readZarr('http://localhost:8081/https://example.com/mydata.z ### Dataset Validation +**⚠️ Note, the results from this may indicate a number of failures in python due to not being able to open the remote stores.** + The project includes scripts to validate dataset compatibility across different versions of the spatialdata library and the JavaScript implementation. + #### Validating with Python -Test publicly available datasets with both Python versions (0.5.0 and 0.6.1): +Test publicly available datasets with all Python versions (0.5.0, 0.6.1, and 0.7.0): ```bash -# Validate all datasets with both Python versions +# Validate all datasets with all Python versions pnpm validate:datasets # Validate with a specific version only pnpm validate:datasets:0.5.0 pnpm validate:datasets:0.6.1 +pnpm validate:datasets:0.7.0 # Validate a specific dataset pnpm validate:datasets -- --dataset "Xenium" @@ -291,7 +306,7 @@ For a complete validation of all datasets across all implementations, use the al ```bash # Run complete validation workflow # This will: -# 1. Test all datasets with Python v0.5.0 and v0.6.1 +# 1. Test all datasets with Python v0.5.0, v0.6.1, and v0.7.0 # 2. Build the packages (if needed) # 3. Test all datasets with JavaScript # 4. Generate comparison reports diff --git a/package.json b/package.json index ff44388..04b87a9 100644 --- a/package.json +++ b/package.json @@ -17,11 +17,13 @@ "test:fixtures:generate": "uv run python/scripts/generate_fixtures.py", "test:fixtures:generate:0.5.0": "uv run python/scripts/generate_fixtures.py --version 0.5.0", "test:fixtures:generate:0.6.1": "uv run python/scripts/generate_fixtures.py --version 0.6.1", + "test:fixtures:generate:0.7.0": "uv run python/scripts/generate_fixtures.py --version 0.7.0", "test:server": "node scripts/test-server.js", "test:proxy": "node scripts/cors-proxy.js", "validate:datasets": "uv run python/scripts/validate_datasets.py", "validate:datasets:0.5.0": "uv run python/scripts/validate_datasets.py --version 0.5.0", "validate:datasets:0.6.1": "uv run python/scripts/validate_datasets.py --version 0.6.1", + "validate:datasets:0.7.0": "uv run python/scripts/validate_datasets.py --version 0.7.0", "validate:datasets:js": "node scripts/validate-datasets-js.js", "validate:all": "bash scripts/validate-all.sh", "lint": "biome check .", diff --git a/packages/core/src/schemas/index.ts b/packages/core/src/schemas/index.ts index 0d39648..0fd07c0 100644 --- a/packages/core/src/schemas/index.ts +++ b/packages/core/src/schemas/index.ts @@ -245,6 +245,11 @@ export type NgffImage = z.infer; * Schema for spatialdata_attrs metadata (common to spatial elements). * Contains version info and other spatialdata-specific metadata. * Note: Transformations are stored at the top level of attrs, not inside spatialdata_attrs. + * + * IMPORTANT: The semantic meaning of `version` varies by element type: + * - For raster elements (images/labels): `version` is the spatialdata library version (e.g., '0.5.0', '0.6.1', '0.7.0') + * and does NOT control OME-NGFF format detection (which is determined by structure). + * - For shapes/points: `version` is the spatialdata format version (e.g., '0.1', '0.2') and IS used for format detection. */ export const spatialDataAttrsSchema = z .object({ @@ -256,9 +261,9 @@ export type SpatialDataAttrs = z.infer; /** * Schema for raster element attrs in spatialdata 0.5.0 format - * Has multiscales at the top level (older OME-NGFF format) + * Uses OME-NGFF 0.4 format with multiscales at the top level */ -const rasterAttrsV050Schema = z +const rasterAttrs_OME_04_Schema = z .object({ multiscales: z .array( @@ -284,9 +289,9 @@ const rasterAttrsV050Schema = z /** * Schema for raster element attrs in spatialdata 0.6.1+ format - * Has multiscales nested under 'ome' key (newer OME-NGFF format) + * Uses OME-NGFF 0.5 format with multiscales nested under 'ome' key */ -const rasterAttrsV061Schema = z +const rasterAttrs_OME_05_Schema = z .object({ ome: z .object({ @@ -316,13 +321,19 @@ const rasterAttrsV061Schema = z /** * Schema for raster element attrs (images & labels) - * Supports both spatialdata 0.5.0 (top-level multiscales) and 0.6.1+ (nested under 'ome') formats. + * Supports both spatialdata 0.5.0 (OME-NGFF 0.4, top-level multiscales) and + * spatialdata 0.6.0+ (OME-NGFF 0.5, nested under 'ome') formats. * Uses zod transform to normalize both formats to a consistent internal representation. + * + * NOTE: Format detection is STRUCTURAL (presence of 'ome' key), NOT based on + * spatialdata_attrs.version. The version field is metadata only and does not control + * which schema is applied. */ export const rasterAttrsSchema = z - .union([rasterAttrsV050Schema, rasterAttrsV061Schema]) + .union([rasterAttrs_OME_04_Schema, rasterAttrs_OME_05_Schema]) .transform((data): RasterAttrs => { - // If it's the v0.6.1+ format (has 'ome' key), extract multiscales and omero from it + // Format detection is structural: if 'ome' key exists, it's OME-NGFF 0.5 format + // This is independent of spatialdata_attrs.version (which is library version metadata) if ('ome' in data && data.ome && typeof data.ome === 'object') { const omeData = data.ome as { multiscales: unknown; diff --git a/python/scripts/generate_fixtures.py b/python/scripts/generate_fixtures.py index bda6c84..15a7083 100644 --- a/python/scripts/generate_fixtures.py +++ b/python/scripts/generate_fixtures.py @@ -3,7 +3,7 @@ Wrapper script to generate test fixtures for all spatialdata versions. This script coordinates running the version-specific fixture generation scripts -located in python/v0.5.0/ and python/v0.6.1/. +located in python/v0.5.0/, python/v0.6.1/, and python/v0.7.0/. """ import argparse @@ -19,9 +19,9 @@ def main(): parser.add_argument( "--version", type=str, - choices=["0.5.0", "0.6.1"], + choices=["0.5.0", "0.6.1", "0.7.0"], default=None, - help="SpatialData version to generate fixtures for (default: both)", + help="SpatialData version to generate fixtures for (default: all)", ) parser.add_argument( "--output-dir", @@ -37,7 +37,7 @@ def main(): project_root = script_path.parent.parent.parent output_dir = project_root / args.output_dir - versions = [args.version] if args.version else ["0.5.0", "0.6.1"] + versions = [args.version] if args.version else ["0.5.0", "0.6.1", "0.7.0"] success = True for version in versions: diff --git a/python/scripts/validate_datasets.py b/python/scripts/validate_datasets.py index 23bf4ce..ecf365a 100755 --- a/python/scripts/validate_datasets.py +++ b/python/scripts/validate_datasets.py @@ -216,22 +216,24 @@ def generate_markdown_table(results: list[ValidationResult]) -> str: lines.append(f"\nGenerated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") lines.append("## Summary") lines.append("") - lines.append("| Dataset | v0.5.0 | v0.6.1 | URL |") - lines.append("|---------|--------|--------|-----|") + lines.append("| Dataset | v0.5.0 | v0.6.1 | v0.7.0 | URL |") + lines.append("|---------|--------|--------|--------|-----|") for dataset_name in sorted(datasets.keys()): versions = datasets[dataset_name] v050 = versions.get("0.5.0") v061 = versions.get("0.6.1") + v070 = versions.get("0.7.0") v050_status = "✅" if v050 and v050.success else "❌" if v050 else "⏭️" v061_status = "✅" if v061 and v061.success else "❌" if v061 else "⏭️" + v070_status = "✅" if v070 and v070.success else "❌" if v070 else "⏭️" # Get URL from first available result - url = (v050 or v061).dataset_url if (v050 or v061) else "" + url = (v050 or v061 or v070).dataset_url if (v050 or v061 or v070) else "" url_short = url.split("spatialdata-sandbox/")[-1] if "spatialdata-sandbox/" in url else url - lines.append(f"| {dataset_name} | {v050_status} | {v061_status} | `{url_short}` |") + lines.append(f"| {dataset_name} | {v050_status} | {v061_status} | {v070_status} | `{url_short}` |") lines.append("") lines.append("Legend: ✅ Success | ❌ Failed | ⏭️ Not tested") @@ -246,7 +248,7 @@ def generate_markdown_table(results: list[ValidationResult]) -> str: lines.append(f"### {dataset_name}") lines.append("") - for version in ["0.5.0", "0.6.1"]: + for version in ["0.5.0", "0.6.1", "0.7.0"]: result = versions.get(version) if not result: continue @@ -339,9 +341,9 @@ def main(): parser.add_argument( "--version", type=str, - choices=["0.5.0", "0.6.1"], + choices=["0.5.0", "0.6.1", "0.7.0"], default=None, - help="SpatialData version to test (default: both)", + help="SpatialData version to test (default: all)", ) parser.add_argument( "--dataset", @@ -396,7 +398,7 @@ def main(): sys.exit(1) # Determine versions to test - versions = [args.version] if args.version else ["0.5.0", "0.6.1"] + versions = [args.version] if args.version else ["0.5.0", "0.6.1", "0.7.0"] # Ensure environments are set up for version in versions: diff --git a/python/v0.6.1/generate_fixtures.py b/python/v0.6.1/generate_fixtures.py index 09ca175..8909b87 100755 --- a/python/v0.6.1/generate_fixtures.py +++ b/python/v0.6.1/generate_fixtures.py @@ -50,25 +50,21 @@ def generate_fixtures(output_dir: Path): # Use spatialdata's write method # The API may vary by version, so we try different methods try: - # Try the standard write method (most common) + # Try the standard write method with overwrite (most common) sdata.write(store_path, overwrite=True) except TypeError: # If overwrite parameter doesn't exist, try without it # (older versions might not support it) sdata.write(store_path) - except (AttributeError, ValueError) as e: - # If write doesn't exist or other error, try alternatives + except ValueError as e: + # Handle "already exists" errors if "already exists" in str(e).lower(): # Shouldn't happen since we removed it, but handle anyway shutil.rmtree(store_path) sdata.write(store_path) else: - # Fallback: try write_zarr if write doesn't exist - try: - sdata.write_zarr(store_path) - except AttributeError: - # Another fallback: use sd.io.write_zarr - sd.io.write_zarr(sdata, store_path) + # Re-raise other ValueError exceptions + raise print(f"✓ Generated fixture at {store_path}") diff --git a/python/v0.7.0/generate_fixtures.py b/python/v0.7.0/generate_fixtures.py new file mode 100755 index 0000000..92a3cb4 --- /dev/null +++ b/python/v0.7.0/generate_fixtures.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +""" +Generate test fixtures using spatialdata library (version 0.7.0). + +This script generates spatialdata zarr stores for testing with spatialdata 0.7.0. +It runs in the python/v0.7.0/ environment which has spatialdata==0.7.0 pinned. +""" + +import sys +from pathlib import Path + +# Add scripts directory to path if we need shared utilities +script_dir = Path(__file__).parent +sys.path.insert(0, str(script_dir.parent / "scripts")) + +from spatialdata.datasets import blobs +import spatialdata as sd + + +def generate_fixtures(output_dir: Path): + """Generate test fixtures for spatialdata version 0.7.0.""" + version = "0.7.0" + print(f"Generating fixtures for spatialdata version {version}...") + + # Verify we're using the correct version + actual_version = sd.__version__ + if actual_version != version: + print(f"⚠️ Warning: Expected version {version} but got {actual_version}") + print(" This may indicate the wrong environment is active.") + + # Create output directory + version_dir = output_dir / f"v{version}" + version_dir.mkdir(parents=True, exist_ok=True) + + # Generate a simple spatialdata object using blobs dataset + print("Creating spatialdata object with blobs dataset...") + sdata = blobs() + + # Save to zarr store + store_path = version_dir / "blobs.zarr" + + # Remove existing store if it exists (to allow regenerating fixtures) + import shutil + if store_path.exists(): + print(f"Removing existing fixture at {store_path}...") + shutil.rmtree(store_path) + + print(f"Saving to {store_path}...") + + # Use spatialdata's write method + # The API may vary by version, so we try different methods + try: + # Try the standard write method with overwrite (most common) + sdata.write(store_path, overwrite=True) + except TypeError: + # If overwrite parameter doesn't exist, try without it + # (older versions might not support it) + sdata.write(store_path) + except ValueError as e: + # Handle "already exists" errors + if "already exists" in str(e).lower(): + # Shouldn't happen since we removed it, but handle anyway + shutil.rmtree(store_path) + sdata.write(store_path) + else: + # Re-raise other ValueError exceptions + raise + + print(f"✓ Generated fixture at {store_path}") + + # Print some metadata about what was generated + print("\nGenerated elements:") + for element_type in ["images", "labels", "points", "shapes", "tables"]: + elements = getattr(sdata, element_type, None) + if elements: + # Handle both dict and list cases + if isinstance(elements, dict): + print(f" - {element_type}: {len(elements)} element(s)") + for name in elements.keys(): + print(f" * {name}") + elif isinstance(elements, list): + print(f" - {element_type}: {len(elements)} element(s)") + for i, elem in enumerate(elements): + print(f" * element_{i}") + else: + print(f" - {element_type}: present") + + if hasattr(sdata, "coordinate_systems"): + if isinstance(sdata.coordinate_systems, dict): + print(f"\nCoordinate systems: {list(sdata.coordinate_systems.keys())}") + elif isinstance(sdata.coordinate_systems, list): + print(f"\nCoordinate systems: {sdata.coordinate_systems}") + else: + print(f"\nCoordinate systems: {sdata.coordinate_systems}") + + print(f"\nUsing spatialdata version {actual_version}") + + return store_path + + +def main(): + import argparse + + parser = argparse.ArgumentParser( + description="Generate test fixtures for SpatialData.ts using spatialdata 0.7.0" + ) + parser.add_argument( + "--output-dir", + type=str, + default="test-fixtures", + help="Output directory for fixtures (default: test-fixtures)", + ) + + args = parser.parse_args() + + # Get project root (parent of python/ directory) + project_root = Path(__file__).parent.parent.parent + output_dir = project_root / args.output_dir + + generate_fixtures(output_dir) + print("\n✓ Fixtures generated successfully!") + + +if __name__ == "__main__": + main() + diff --git a/python/v0.7.0/pyproject.toml b/python/v0.7.0/pyproject.toml new file mode 100644 index 0000000..c55a906 --- /dev/null +++ b/python/v0.7.0/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "spatialdata-test-fixtures-v0.7.0" +version = "0.1.0" +description = "Test fixture generation for SpatialData.ts using spatialdata 0.7.0" +requires-python = ">=3.12" +dependencies = [ + "spatialdata==0.7.0", +] + diff --git a/python/v0.7.0/uv.lock b/python/v0.7.0/uv.lock new file mode 100644 index 0000000..4267c86 --- /dev/null +++ b/python/v0.7.0/uv.lock @@ -0,0 +1,2102 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "aiobotocore" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "aioitertools" }, + { name = "botocore" }, + { name = "jmespath" }, + { name = "multidict" }, + { name = "python-dateutil" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/bc/00ac3f44a66661fb28f2425b056d5bd202c2269a686ab0a683bb0e0516f0/aiobotocore-3.1.1.tar.gz", hash = "sha256:a19a36b930a041aa21553d67ae8a6bc464e107806eee60af3c71502f1009826c", size = 122530, upload-time = "2026-01-20T17:00:29.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/84/d844b79acd9fe15ded60b614b7df04a12fad854ee1fbb8415d726ab1beeb/aiobotocore-3.1.1-py3-none-any.whl", hash = "sha256:a4e12a3bd099cd19dc2b2e9fe01a807131b46ebd0f83f509bda3cb243e988c32", size = 87667, upload-time = "2026-01-20T17:00:27.869Z" }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, + { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, + { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, + { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, + { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, + { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, + { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, + { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, + { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, + { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, + { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, + { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, + { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, + { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, + { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" }, + { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" }, + { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" }, + { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" }, + { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" }, + { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" }, + { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" }, + { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" }, + { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" }, + { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" }, + { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" }, + { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" }, + { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" }, + { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" }, + { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" }, + { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" }, +] + +[[package]] +name = "aioitertools" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/3c/53c4a17a05fb9ea2313ee1777ff53f5e001aefd5cc85aa2f4c2d982e1e38/aioitertools-0.13.0.tar.gz", hash = "sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c", size = 19322, upload-time = "2025-11-06T22:17:07.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl", hash = "sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be", size = 24182, upload-time = "2025-11-06T22:17:06.502Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "anndata" +version = "0.12.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "array-api-compat" }, + { name = "h5py" }, + { name = "legacy-api-wrap" }, + { name = "natsort" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "scipy" }, + { name = "zarr" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/80/00831f1683b152f19385bce64b9bf17866c271b8a33b399c3f0632bdb756/anndata-0.12.9.tar.gz", hash = "sha256:50c89ac662d37b974dd61115b729e543cbe8c1f4358b4e4361763b8a08c724eb", size = 2253867, upload-time = "2026-01-29T12:21:42.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/3e/4145538947c0149f0969666334fe91cab45a5a91cf6a35d865a67e283e4c/anndata-0.12.9-py3-none-any.whl", hash = "sha256:61410463bc6abdb173ef392735f0b2e5456cb002aafaaaf9b3a8ed401e3cc0e9", size = 176166, upload-time = "2026-01-29T12:21:39.995Z" }, +] + +[[package]] +name = "annsel" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anndata" }, + { name = "more-itertools" }, + { name = "narwhals", extra = ["pandas"] }, + { name = "pooch" }, + { name = "session-info2" }, + { name = "tqdm" }, + { name = "universal-pathlib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/93/cdbe4903acb43fd743832e0c2933b88ec2dfd2a87b147992c45b90f647c8/annsel-0.1.2.tar.gz", hash = "sha256:a9ab14495f3e28c216a5cb74f918542710ac82e22bb04db57af233bbbf536da8", size = 341977, upload-time = "2026-01-06T00:07:19.823Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/43/fff37380b704b60fad17c298b631044d98e3db8b9721df7897c967facf51/annsel-0.1.2-py3-none-any.whl", hash = "sha256:9659f60f4ee5545bc030341b6b8e932e0a615b95ac83d9cb6e2bd50951ae26da", size = 19120, upload-time = "2026-01-06T00:07:18.69Z" }, +] + +[[package]] +name = "array-api-compat" +version = "1.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/36/f799b36d7025a92a23819f9f06541babdb84b6fd0bd4253f8be2eca348a4/array_api_compat-1.13.0.tar.gz", hash = "sha256:8b83a56aa8b9477472fee37f7731968dd213e20c198a05ac49caeff9b03f48a6", size = 103065, upload-time = "2025-12-28T11:26:57.734Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/5d/493b1b5528ab5072feae30821ff3a07b7a0474213d548efb1fdf135f85c1/array_api_compat-1.13.0-py3-none-any.whl", hash = "sha256:c15026a0ddec42815383f07da285472e1b1ff2e632eb7afbcfe9b08fcbad9bf1", size = 58585, upload-time = "2025-12-28T11:26:56.081Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "botocore" +version = "1.42.30" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/38/23862628a0eb044c8b8b3d7a9ad1920b3bfd6bce6d746d5a871e8382c7e4/botocore-1.42.30.tar.gz", hash = "sha256:9bf1662b8273d5cc3828a49f71ca85abf4e021011c1f0a71f41a2ea5769a5116", size = 14891439, upload-time = "2026-01-16T20:37:13.77Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/8d/6d7b016383b1f74dd93611b1c5078bbaddaca901553ab886dcda87cae365/botocore-1.42.30-py3-none-any.whl", hash = "sha256:97070a438cac92430bb7b65f8ebd7075224f4a289719da4ee293d22d1e98db02", size = 14566340, upload-time = "2026-01-16T20:37:10.94Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "colorcet" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/c3/ae78e10b7139d6b7ce080d2e81d822715763336aa4229720f49cb3b3e15b/colorcet-3.1.0.tar.gz", hash = "sha256:2921b3cd81a2288aaf2d63dbc0ce3c26dcd882e8c389cc505d6886bf7aa9a4eb", size = 2183107, upload-time = "2024-02-29T19:15:42.976Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/c6/9963d588cc3d75d766c819e0377a168ef83cf3316a92769971527a1ad1de/colorcet-3.1.0-py3-none-any.whl", hash = "sha256:2a7d59cc8d0f7938eeedd08aad3152b5319b4ba3bcb7a612398cc17a384cb296", size = 260286, upload-time = "2024-02-29T19:15:40.494Z" }, +] + +[[package]] +name = "dask" +version = "2026.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "cloudpickle" }, + { name = "fsspec" }, + { name = "packaging" }, + { name = "partd" }, + { name = "pyyaml" }, + { name = "toolz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/46/61ecde57bac647ca7eb6ffef8dcd90af6c1c649020874cd7fd8738003d62/dask-2026.1.1.tar.gz", hash = "sha256:12b1dbb0d6e92f287feb4076871600b2fba3a843d35ff214776ada5e9e7a1529", size = 10994732, upload-time = "2026-01-16T12:35:30.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/4b/9cc373120658a2516aa5f6dcdde631c95d714b876d29ad8f8e009d793f3f/dask-2026.1.1-py3-none-any.whl", hash = "sha256:146b0ef2918eb581e06139183a88801b4a8c52d7c37758a91f8c3b75c54b0e15", size = 1481492, upload-time = "2026-01-16T12:35:22.602Z" }, +] + +[package.optional-dependencies] +array = [ + { name = "numpy" }, +] +dataframe = [ + { name = "numpy" }, + { name = "pandas" }, + { name = "pyarrow" }, +] + +[[package]] +name = "dask-image" +version = "2025.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dask", extra = ["array", "dataframe"] }, + { name = "numpy" }, + { name = "pandas" }, + { name = "pims" }, + { name = "scipy" }, + { name = "tifffile" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/c4/7b83217443201469384a415687a8b89da8e55fc7a182e9507a69851a78b9/dask_image-2025.11.0.tar.gz", hash = "sha256:45cf1a9c3a8a1c143c75d43f1494e4fe0827564d3ec6efb93618fb04603ba0b3", size = 79561, upload-time = "2025-11-13T01:57:28.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/4b/817da308fa1170da07ef01259585887a3bbb6ab80700b3e61ce4967301ec/dask_image-2025.11.0-py3-none-any.whl", hash = "sha256:4834ece8d7133f8cd7d4e672f7f5a598c9057e687b20f14f3121360e3e1690b4", size = 61936, upload-time = "2025-11-13T01:57:27.133Z" }, +] + +[[package]] +name = "datashader" +version = "0.18.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorcet" }, + { name = "multipledispatch" }, + { name = "numba" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "param" }, + { name = "pyct" }, + { name = "requests" }, + { name = "scipy" }, + { name = "toolz" }, + { name = "xarray" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/5d/3f8db3f4fa141c577eb8953323670d91e6c18bd135bf2346fb02ae0b1768/datashader-0.18.2.tar.gz", hash = "sha256:53ef0bc35b9ba1034bdf290a2e28babf0fa2b075a3e5a822c79dcde12183d1ff", size = 18195212, upload-time = "2025-08-05T10:04:37.793Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/0e/b11ad5fd77e3dd0baad9cac3184315be7654ae401e3b0b0c324503f23d96/datashader-0.18.2-py3-none-any.whl", hash = "sha256:2aa90e867a46b1e75248f32a47c5b14bb5dc869524152f88c0af8369d47359e7", size = 18330272, upload-time = "2025-08-05T10:04:35.288Z" }, +] + +[[package]] +name = "donfig" +version = "0.8.1.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/71/80cc718ff6d7abfbabacb1f57aaa42e9c1552bfdd01e64ddd704e4a03638/donfig-0.8.1.post1.tar.gz", hash = "sha256:3bef3413a4c1c601b585e8d297256d0c1470ea012afa6e8461dc28bfb7c23f52", size = 19506, upload-time = "2024-05-23T14:14:31.513Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl", hash = "sha256:2a3175ce74a06109ff9307d90a230f81215cbac9a751f4d1c6194644b8204f9d", size = 21592, upload-time = "2024-05-23T14:13:55.283Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/7d/5df2650c57d47c57232af5ef4b4fdbff182070421e405e0d62c6cdbfaa87/fsspec-2026.1.0.tar.gz", hash = "sha256:e987cb0496a0d81bba3a9d1cee62922fb395e7d4c3b575e57f547953334fe07b", size = 310496, upload-time = "2026-01-09T15:21:35.562Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/c9/97cc5aae1648dcb851958a3ddf73ccd7dbe5650d95203ecb4d7720b4cdbf/fsspec-2026.1.0-py3-none-any.whl", hash = "sha256:cb76aa913c2285a3b49bdd5fc55b1d7c708d7208126b60f2eb8194fe1b4cbdcc", size = 201838, upload-time = "2026-01-09T15:21:34.041Z" }, +] + +[package.optional-dependencies] +http = [ + { name = "aiohttp" }, +] +s3 = [ + { name = "s3fs" }, +] + +[[package]] +name = "geopandas" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pyogrio" }, + { name = "pyproj" }, + { name = "shapely" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/24/5eb5685d7bf89d64218919379f882d19a60f8219d66d833c83b1cf264c95/geopandas-1.1.2.tar.gz", hash = "sha256:33f7b33565c46a45b8459a2ab699ec943fdbb5716e58e251b3c413cf7783106c", size = 336037, upload-time = "2025-12-22T21:06:13.749Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/e4/fac19dc34cb686c96011388b813ff7b858a70681e5ce6ce7698e5021b0f4/geopandas-1.1.2-py3-none-any.whl", hash = "sha256:2bb0b1052cb47378addb4ba54c47f8d4642dcbda9b61375638274f49d9f0bb0d", size = 341734, upload-time = "2025-12-22T21:06:12.498Z" }, +] + +[[package]] +name = "google-crc32c" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz", hash = "sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79", size = 14192, upload-time = "2025-12-16T00:35:25.142Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/5f/7307325b1198b59324c0fa9807cafb551afb65e831699f2ce211ad5c8240/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113", size = 31300, upload-time = "2025-12-16T00:21:56.723Z" }, + { url = "https://files.pythonhosted.org/packages/21/8e/58c0d5d86e2220e6a37befe7e6a94dd2f6006044b1a33edf1ff6d9f7e319/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb", size = 30867, upload-time = "2025-12-16T00:38:31.302Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a9/a780cc66f86335a6019f557a8aaca8fbb970728f0efd2430d15ff1beae0e/google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411", size = 33364, upload-time = "2025-12-16T00:40:22.96Z" }, + { url = "https://files.pythonhosted.org/packages/21/3f/3457ea803db0198c9aaca2dd373750972ce28a26f00544b6b85088811939/google_crc32c-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454", size = 33740, upload-time = "2025-12-16T00:40:23.96Z" }, + { url = "https://files.pythonhosted.org/packages/df/c0/87c2073e0c72515bb8733d4eef7b21548e8d189f094b5dad20b0ecaf64f6/google_crc32c-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962", size = 34437, upload-time = "2025-12-16T00:35:21.395Z" }, + { url = "https://files.pythonhosted.org/packages/d1/db/000f15b41724589b0e7bc24bc7a8967898d8d3bc8caf64c513d91ef1f6c0/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:3ebb04528e83b2634857f43f9bb8ef5b2bbe7f10f140daeb01b58f972d04736b", size = 31297, upload-time = "2025-12-16T00:23:20.709Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/8ebed0c39c53a7e838e2a486da8abb0e52de135f1b376ae2f0b160eb4c1a/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:450dc98429d3e33ed2926fc99ee81001928d63460f8538f21a5d6060912a8e27", size = 30867, upload-time = "2025-12-16T00:43:14.628Z" }, + { url = "https://files.pythonhosted.org/packages/ce/42/b468aec74a0354b34c8cbf748db20d6e350a68a2b0912e128cabee49806c/google_crc32c-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa", size = 33344, upload-time = "2025-12-16T00:40:24.742Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e8/b33784d6fc77fb5062a8a7854e43e1e618b87d5ddf610a88025e4de6226e/google_crc32c-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8", size = 33694, upload-time = "2025-12-16T00:40:25.505Z" }, + { url = "https://files.pythonhosted.org/packages/92/b1/d3cbd4d988afb3d8e4db94ca953df429ed6db7282ed0e700d25e6c7bfc8d/google_crc32c-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f", size = 34435, upload-time = "2025-12-16T00:35:22.107Z" }, + { url = "https://files.pythonhosted.org/packages/21/88/8ecf3c2b864a490b9e7010c84fd203ec8cf3b280651106a3a74dd1b0ca72/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:e6584b12cb06796d285d09e33f63309a09368b9d806a551d8036a4207ea43697", size = 31301, upload-time = "2025-12-16T00:24:48.527Z" }, + { url = "https://files.pythonhosted.org/packages/36/c6/f7ff6c11f5ca215d9f43d3629163727a272eabc356e5c9b2853df2bfe965/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:f4b51844ef67d6cf2e9425983274da75f18b1597bb2c998e1c0a0e8d46f8f651", size = 30868, upload-time = "2025-12-16T00:48:12.163Z" }, + { url = "https://files.pythonhosted.org/packages/56/15/c25671c7aad70f8179d858c55a6ae8404902abe0cdcf32a29d581792b491/google_crc32c-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b0d1a7afc6e8e4635564ba8aa5c0548e3173e41b6384d7711a9123165f582de2", size = 33381, upload-time = "2025-12-16T00:40:26.268Z" }, + { url = "https://files.pythonhosted.org/packages/42/fa/f50f51260d7b0ef5d4898af122d8a7ec5a84e2984f676f746445f783705f/google_crc32c-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3f68782f3cbd1bce027e48768293072813469af6a61a86f6bb4977a4380f21", size = 33734, upload-time = "2025-12-16T00:40:27.028Z" }, + { url = "https://files.pythonhosted.org/packages/08/a5/7b059810934a09fb3ccb657e0843813c1fee1183d3bc2c8041800374aa2c/google_crc32c-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:d511b3153e7011a27ab6ee6bb3a5404a55b994dc1a7322c0b87b29606d9790e2", size = 34878, upload-time = "2025-12-16T00:35:23.142Z" }, +] + +[[package]] +name = "h5py" +version = "3.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/6a/0d79de0b025aa85dc8864de8e97659c94cf3d23148394a954dc5ca52f8c8/h5py-3.15.1.tar.gz", hash = "sha256:c86e3ed45c4473564de55aa83b6fc9e5ead86578773dfbd93047380042e26b69", size = 426236, upload-time = "2025-10-16T10:35:27.404Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/b8/c0d9aa013ecfa8b7057946c080c0c07f6fa41e231d2e9bd306a2f8110bdc/h5py-3.15.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:316dd0f119734f324ca7ed10b5627a2de4ea42cc4dfbcedbee026aaa361c238c", size = 3399089, upload-time = "2025-10-16T10:34:12.135Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5e/3c6f6e0430813c7aefe784d00c6711166f46225f5d229546eb53032c3707/h5py-3.15.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b51469890e58e85d5242e43aab29f5e9c7e526b951caab354f3ded4ac88e7b76", size = 2847803, upload-time = "2025-10-16T10:34:14.564Z" }, + { url = "https://files.pythonhosted.org/packages/00/69/ba36273b888a4a48d78f9268d2aee05787e4438557450a8442946ab8f3ec/h5py-3.15.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a33bfd5dfcea037196f7778534b1ff7e36a7f40a89e648c8f2967292eb6898e", size = 4914884, upload-time = "2025-10-16T10:34:18.452Z" }, + { url = "https://files.pythonhosted.org/packages/3a/30/d1c94066343a98bb2cea40120873193a4fed68c4ad7f8935c11caf74c681/h5py-3.15.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25c8843fec43b2cc368aa15afa1cdf83fc5e17b1c4e10cd3771ef6c39b72e5ce", size = 5109965, upload-time = "2025-10-16T10:34:21.853Z" }, + { url = "https://files.pythonhosted.org/packages/81/3d/d28172116eafc3bc9f5991b3cb3fd2c8a95f5984f50880adfdf991de9087/h5py-3.15.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a308fd8681a864c04423c0324527237a0484e2611e3441f8089fd00ed56a8171", size = 4561870, upload-time = "2025-10-16T10:34:26.69Z" }, + { url = "https://files.pythonhosted.org/packages/a5/83/393a7226024238b0f51965a7156004eaae1fcf84aa4bfecf7e582676271b/h5py-3.15.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f4a016df3f4a8a14d573b496e4d1964deb380e26031fc85fb40e417e9131888a", size = 5037161, upload-time = "2025-10-16T10:34:30.383Z" }, + { url = "https://files.pythonhosted.org/packages/cf/51/329e7436bf87ca6b0fe06dd0a3795c34bebe4ed8d6c44450a20565d57832/h5py-3.15.1-cp312-cp312-win_amd64.whl", hash = "sha256:59b25cf02411bf12e14f803fef0b80886444c7fe21a5ad17c6a28d3f08098a1e", size = 2874165, upload-time = "2025-10-16T10:34:33.461Z" }, + { url = "https://files.pythonhosted.org/packages/09/a8/2d02b10a66747c54446e932171dd89b8b4126c0111b440e6bc05a7c852ec/h5py-3.15.1-cp312-cp312-win_arm64.whl", hash = "sha256:61d5a58a9851e01ee61c932bbbb1c98fe20aba0a5674776600fb9a361c0aa652", size = 2458214, upload-time = "2025-10-16T10:34:35.733Z" }, + { url = "https://files.pythonhosted.org/packages/88/b3/40207e0192415cbff7ea1d37b9f24b33f6d38a5a2f5d18a678de78f967ae/h5py-3.15.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c8440fd8bee9500c235ecb7aa1917a0389a2adb80c209fa1cc485bd70e0d94a5", size = 3376511, upload-time = "2025-10-16T10:34:38.596Z" }, + { url = "https://files.pythonhosted.org/packages/31/96/ba99a003c763998035b0de4c299598125df5fc6c9ccf834f152ddd60e0fb/h5py-3.15.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ab2219dbc6fcdb6932f76b548e2b16f34a1f52b7666e998157a4dfc02e2c4123", size = 2826143, upload-time = "2025-10-16T10:34:41.342Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c2/fc6375d07ea3962df7afad7d863fe4bde18bb88530678c20d4c90c18de1d/h5py-3.15.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8cb02c3a96255149ed3ac811eeea25b655d959c6dd5ce702c9a95ff11859eb5", size = 4908316, upload-time = "2025-10-16T10:34:44.619Z" }, + { url = "https://files.pythonhosted.org/packages/d9/69/4402ea66272dacc10b298cca18ed73e1c0791ff2ae9ed218d3859f9698ac/h5py-3.15.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:121b2b7a4c1915d63737483b7bff14ef253020f617c2fb2811f67a4bed9ac5e8", size = 5103710, upload-time = "2025-10-16T10:34:48.639Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f6/11f1e2432d57d71322c02a97a5567829a75f223a8c821764a0e71a65cde8/h5py-3.15.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59b0d63b318bf3cc06687def2b45afd75926bbc006f7b8cd2b1a231299fc8599", size = 4556042, upload-time = "2025-10-16T10:34:51.841Z" }, + { url = "https://files.pythonhosted.org/packages/18/88/3eda3ef16bfe7a7dbc3d8d6836bbaa7986feb5ff091395e140dc13927bcc/h5py-3.15.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e02fe77a03f652500d8bff288cbf3675f742fc0411f5a628fa37116507dc7cc0", size = 5030639, upload-time = "2025-10-16T10:34:55.257Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ea/fbb258a98863f99befb10ed727152b4ae659f322e1d9c0576f8a62754e81/h5py-3.15.1-cp313-cp313-win_amd64.whl", hash = "sha256:dea78b092fd80a083563ed79a3171258d4a4d307492e7cf8b2313d464c82ba52", size = 2864363, upload-time = "2025-10-16T10:34:58.099Z" }, + { url = "https://files.pythonhosted.org/packages/5d/c9/35021cc9cd2b2915a7da3026e3d77a05bed1144a414ff840953b33937fb9/h5py-3.15.1-cp313-cp313-win_arm64.whl", hash = "sha256:c256254a8a81e2bddc0d376e23e2a6d2dc8a1e8a2261835ed8c1281a0744cd97", size = 2449570, upload-time = "2025-10-16T10:35:00.473Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2c/926eba1514e4d2e47d0e9eb16c784e717d8b066398ccfca9b283917b1bfb/h5py-3.15.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5f4fb0567eb8517c3ecd6b3c02c4f4e9da220c8932604960fd04e24ee1254763", size = 3380368, upload-time = "2025-10-16T10:35:03.117Z" }, + { url = "https://files.pythonhosted.org/packages/65/4b/d715ed454d3baa5f6ae1d30b7eca4c7a1c1084f6a2edead9e801a1541d62/h5py-3.15.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:954e480433e82d3872503104f9b285d369048c3a788b2b1a00e53d1c47c98dd2", size = 2833793, upload-time = "2025-10-16T10:35:05.623Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d4/ef386c28e4579314610a8bffebbee3b69295b0237bc967340b7c653c6c10/h5py-3.15.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd125c131889ebbef0849f4a0e29cf363b48aba42f228d08b4079913b576bb3a", size = 4903199, upload-time = "2025-10-16T10:35:08.972Z" }, + { url = "https://files.pythonhosted.org/packages/33/5d/65c619e195e0b5e54ea5a95c1bb600c8ff8715e0d09676e4cce56d89f492/h5py-3.15.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28a20e1a4082a479b3d7db2169f3a5034af010b90842e75ebbf2e9e49eb4183e", size = 5097224, upload-time = "2025-10-16T10:35:12.808Z" }, + { url = "https://files.pythonhosted.org/packages/30/30/5273218400bf2da01609e1292f562c94b461fcb73c7a9e27fdadd43abc0a/h5py-3.15.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa8df5267f545b4946df8ca0d93d23382191018e4cda2deda4c2cedf9a010e13", size = 4551207, upload-time = "2025-10-16T10:35:16.24Z" }, + { url = "https://files.pythonhosted.org/packages/d3/39/a7ef948ddf4d1c556b0b2b9559534777bccc318543b3f5a1efdf6b556c9c/h5py-3.15.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99d374a21f7321a4c6ab327c4ab23bd925ad69821aeb53a1e75dd809d19f67fa", size = 5025426, upload-time = "2025-10-16T10:35:19.831Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d8/7368679b8df6925b8415f9dcc9ab1dab01ddc384d2b2c24aac9191bd9ceb/h5py-3.15.1-cp314-cp314-win_amd64.whl", hash = "sha256:9c73d1d7cdb97d5b17ae385153472ce118bed607e43be11e9a9deefaa54e0734", size = 2865704, upload-time = "2025-10-16T10:35:22.658Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b7/4a806f85d62c20157e62e58e03b27513dc9c55499768530acc4f4c5ce4be/h5py-3.15.1-cp314-cp314-win_arm64.whl", hash = "sha256:a6d8c5a05a76aca9a494b4c53ce8a9c29023b7f64f625c6ce1841e92a362ccdf", size = 2465544, upload-time = "2025-10-16T10:35:25.695Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "imageio" +version = "2.37.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/6f/606be632e37bf8d05b253e8626c2291d74c691ddc7bcdf7d6aaf33b32f6a/imageio-2.37.2.tar.gz", hash = "sha256:0212ef2727ac9caa5ca4b2c75ae89454312f440a756fcfc8ef1993e718f50f8a", size = 389600, upload-time = "2025-11-04T14:29:39.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/fe/301e0936b79bcab4cacc7548bf2853fc28dced0a578bab1f7ef53c9aa75b/imageio-2.37.2-py3-none-any.whl", hash = "sha256:ad9adfb20335d718c03de457358ed69f141021a333c40a53e57273d8a5bd0b9b", size = 317646, upload-time = "2025-11-04T14:29:37.948Z" }, +] + +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + +[[package]] +name = "lazy-loader" +version = "0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/6b/c875b30a1ba490860c93da4cabf479e03f584eba06fe5963f6f6644653d8/lazy_loader-0.4.tar.gz", hash = "sha256:47c75182589b91a4e1a85a136c074285a5ad4d9f39c63e0d7fb76391c4574cd1", size = 15431, upload-time = "2024-04-05T13:03:12.261Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/60/d497a310bde3f01cb805196ac61b7ad6dc5dcf8dce66634dc34364b20b4f/lazy_loader-0.4-py3-none-any.whl", hash = "sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc", size = 12097, upload-time = "2024-04-05T13:03:10.514Z" }, +] + +[[package]] +name = "legacy-api-wrap" +version = "1.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/49/f06f94048c8974205730d40beca879e43b6eee08efb0101cfb8623e60f41/legacy_api_wrap-1.5.tar.gz", hash = "sha256:b41ba6532f3ebfe3a897a35a7f97dec3be04b92a450f6c2bcf89f1b91c9cadf2", size = 11610, upload-time = "2025-11-03T13:21:12.437Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/5b/058db09c45ba58a7321bdf2294cae651b37d6fec68117265af90cde043b0/legacy_api_wrap-1.5-py3-none-any.whl", hash = "sha256:5a8ea50e3e3bcbcdec3447b77034fd0d32cb2cf4089db799238708e4d7e0098d", size = 10182, upload-time = "2025-11-03T13:21:11.102Z" }, +] + +[[package]] +name = "llvmlite" +version = "0.46.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/cd/08ae687ba099c7e3d21fe2ea536500563ef1943c5105bf6ab4ee3829f68e/llvmlite-0.46.0.tar.gz", hash = "sha256:227c9fd6d09dce2783c18b754b7cd9d9b3b3515210c46acc2d3c5badd9870ceb", size = 193456, upload-time = "2025-12-08T18:15:36.295Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/f8/4db016a5e547d4e054ff2f3b99203d63a497465f81ab78ec8eb2ff7b2304/llvmlite-0.46.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b9588ad4c63b4f0175a3984b85494f0c927c6b001e3a246a3a7fb3920d9a137", size = 37232767, upload-time = "2025-12-08T18:15:00.737Z" }, + { url = "https://files.pythonhosted.org/packages/aa/85/4890a7c14b4fa54400945cb52ac3cd88545bbdb973c440f98ca41591cdc5/llvmlite-0.46.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3535bd2bb6a2d7ae4012681ac228e5132cdb75fefb1bcb24e33f2f3e0c865ed4", size = 56275176, upload-time = "2025-12-08T18:15:03.936Z" }, + { url = "https://files.pythonhosted.org/packages/6a/07/3d31d39c1a1a08cd5337e78299fca77e6aebc07c059fbd0033e3edfab45c/llvmlite-0.46.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cbfd366e60ff87ea6cc62f50bc4cd800ebb13ed4c149466f50cf2163a473d1e", size = 55128630, upload-time = "2025-12-08T18:15:07.196Z" }, + { url = "https://files.pythonhosted.org/packages/2a/6b/d139535d7590a1bba1ceb68751bef22fadaa5b815bbdf0e858e3875726b2/llvmlite-0.46.0-cp312-cp312-win_amd64.whl", hash = "sha256:398b39db462c39563a97b912d4f2866cd37cba60537975a09679b28fbbc0fb38", size = 38138940, upload-time = "2025-12-08T18:15:10.162Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ff/3eba7eb0aed4b6fca37125387cd417e8c458e750621fce56d2c541f67fa8/llvmlite-0.46.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:30b60892d034bc560e0ec6654737aaa74e5ca327bd8114d82136aa071d611172", size = 37232767, upload-time = "2025-12-08T18:15:13.22Z" }, + { url = "https://files.pythonhosted.org/packages/0e/54/737755c0a91558364b9200702c3c9c15d70ed63f9b98a2c32f1c2aa1f3ba/llvmlite-0.46.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6cc19b051753368a9c9f31dc041299059ee91aceec81bd57b0e385e5d5bf1a54", size = 56275176, upload-time = "2025-12-08T18:15:16.339Z" }, + { url = "https://files.pythonhosted.org/packages/e6/91/14f32e1d70905c1c0aa4e6609ab5d705c3183116ca02ac6df2091868413a/llvmlite-0.46.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bca185892908f9ede48c0acd547fe4dc1bafefb8a4967d47db6cf664f9332d12", size = 55128629, upload-time = "2025-12-08T18:15:19.493Z" }, + { url = "https://files.pythonhosted.org/packages/4a/a7/d526ae86708cea531935ae777b6dbcabe7db52718e6401e0fb9c5edea80e/llvmlite-0.46.0-cp313-cp313-win_amd64.whl", hash = "sha256:67438fd30e12349ebb054d86a5a1a57fd5e87d264d2451bcfafbbbaa25b82a35", size = 38138941, upload-time = "2025-12-08T18:15:22.536Z" }, + { url = "https://files.pythonhosted.org/packages/95/ae/af0ffb724814cc2ea64445acad05f71cff5f799bb7efb22e47ee99340dbc/llvmlite-0.46.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:d252edfb9f4ac1fcf20652258e3f102b26b03eef738dc8a6ffdab7d7d341d547", size = 37232768, upload-time = "2025-12-08T18:15:25.055Z" }, + { url = "https://files.pythonhosted.org/packages/c9/19/5018e5352019be753b7b07f7759cdabb69ca5779fea2494be8839270df4c/llvmlite-0.46.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:379fdd1c59badeff8982cb47e4694a6143bec3bb49aa10a466e095410522064d", size = 56275173, upload-time = "2025-12-08T18:15:28.109Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c9/d57877759d707e84c082163c543853245f91b70c804115a5010532890f18/llvmlite-0.46.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e8cbfff7f6db0fa2c771ad24154e2a7e457c2444d7673e6de06b8b698c3b269", size = 55128628, upload-time = "2025-12-08T18:15:31.098Z" }, + { url = "https://files.pythonhosted.org/packages/30/a8/e61a8c2b3cc7a597073d9cde1fcbb567e9d827f1db30c93cf80422eac70d/llvmlite-0.46.0-cp314-cp314-win_amd64.whl", hash = "sha256:7821eda3ec1f18050f981819756631d60b6d7ab1a6cf806d9efefbe3f4082d61", size = 39153056, upload-time = "2025-12-08T18:15:33.938Z" }, +] + +[[package]] +name = "locket" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/83/97b29fe05cb6ae28d2dbd30b81e2e402a3eed5f460c26e9eaa5895ceacf5/locket-1.0.0.tar.gz", hash = "sha256:5c0d4c052a8bbbf750e056a8e65ccd309086f4f0f18a2eac306a8dfa4112a632", size = 4350, upload-time = "2022-04-20T22:04:44.312Z" } +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 = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "more-itertools" +version = "10.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "multipledispatch" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/3e/a62c3b824c7dec33c4a1578bcc842e6c30300051033a4e5975ed86cc2536/multipledispatch-1.0.0.tar.gz", hash = "sha256:5c839915465c68206c3e9c473357908216c28383b425361e5d144594bf85a7e0", size = 12385, upload-time = "2023-06-27T16:45:11.074Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/c0/00c9809d8b9346eb238a6bbd5f83e846a4ce4503da94a4c08cb7284c325b/multipledispatch-1.0.0-py3-none-any.whl", hash = "sha256:0c53cd8b077546da4e48869f49b13164bebafd0c2a5afceb6bb6a316e7fb46e4", size = 12818, upload-time = "2023-06-27T16:45:09.418Z" }, +] + +[[package]] +name = "multiscale-spatial-image" +version = "2.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dask" }, + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "spatial-image" }, + { name = "xarray" }, + { name = "xarray-dataclass" }, + { name = "zarr" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/93/8cab41d5f2d11834e4d077d7a9dd7cdf74596fdc68290e31e77422d4edf2/multiscale_spatial_image-2.0.3.tar.gz", hash = "sha256:0b72b20785c5c8bbae7aea0608ca53e02d61fd76720b10473aeafb97e1616cab", size = 1310669, upload-time = "2025-08-07T17:06:58.697Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/a7/fec56dbac873a18930b2127d400794a91dd53898bff811aa4802ddbbfac9/multiscale_spatial_image-2.0.3-py3-none-any.whl", hash = "sha256:8b20d36e90a104083fe14f5466d582eea453c213d1a368d7e7435b3e13445f61", size = 29169, upload-time = "2025-08-07T17:07:00.426Z" }, +] + +[[package]] +name = "narwhals" +version = "2.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/6d/b57c64e5038a8cf071bce391bb11551657a74558877ac961e7fa905ece27/narwhals-2.15.0.tar.gz", hash = "sha256:a9585975b99d95084268445a1fdd881311fa26ef1caa18020d959d5b2ff9a965", size = 603479, upload-time = "2026-01-06T08:10:13.27Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/2e/cf2ffeb386ac3763526151163ad7da9f1b586aac96d2b4f7de1eaebf0c61/narwhals-2.15.0-py3-none-any.whl", hash = "sha256:cbfe21ca19d260d9fd67f995ec75c44592d1f106933b03ddd375df7ac841f9d6", size = 432856, upload-time = "2026-01-06T08:10:11.511Z" }, +] + +[package.optional-dependencies] +pandas = [ + { name = "pandas" }, +] + +[[package]] +name = "natsort" +version = "8.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/a9/a0c57aee75f77794adaf35322f8b6404cbd0f89ad45c87197a937764b7d0/natsort-8.4.0.tar.gz", hash = "sha256:45312c4a0e5507593da193dedd04abb1469253b601ecaf63445ad80f0a1ea581", size = 76575, upload-time = "2023-06-20T04:17:19.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/82/7a9d0550484a62c6da82858ee9419f3dd1ccc9aa1c26a1e43da3ecd20b0d/natsort-8.4.0-py3-none-any.whl", hash = "sha256:4732914fb471f56b5cce04d7bae6f164a592c7712e1c85f9ef585e197299521c", size = 38268, upload-time = "2023-06-20T04:17:17.522Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "numba" +version = "0.63.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llvmlite" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/60/0145d479b2209bd8fdae5f44201eceb8ce5a23e0ed54c71f57db24618665/numba-0.63.1.tar.gz", hash = "sha256:b320aa675d0e3b17b40364935ea52a7b1c670c9037c39cf92c49502a75902f4b", size = 2761666, upload-time = "2025-12-10T02:57:39.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/9c/c0974cd3d00ff70d30e8ff90522ba5fbb2bcee168a867d2321d8d0457676/numba-0.63.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2819cd52afa5d8d04e057bdfd54367575105f8829350d8fb5e4066fb7591cc71", size = 2680981, upload-time = "2025-12-10T02:57:17.579Z" }, + { url = "https://files.pythonhosted.org/packages/cb/70/ea2bc45205f206b7a24ee68a159f5097c9ca7e6466806e7c213587e0c2b1/numba-0.63.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5cfd45dbd3d409e713b1ccfdc2ee72ca82006860254429f4ef01867fdba5845f", size = 3801656, upload-time = "2025-12-10T02:57:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/4f4ba4fd0f99825cbf3cdefd682ca3678be1702b63362011de6e5f71f831/numba-0.63.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69a599df6976c03b7ecf15d05302696f79f7e6d10d620367407517943355bcb0", size = 3501857, upload-time = "2025-12-10T02:57:20.721Z" }, + { url = "https://files.pythonhosted.org/packages/af/fd/6540456efa90b5f6604a86ff50dabefb187e43557e9081adcad3be44f048/numba-0.63.1-cp312-cp312-win_amd64.whl", hash = "sha256:bbad8c63e4fc7eb3cdb2c2da52178e180419f7969f9a685f283b313a70b92af3", size = 2750282, upload-time = "2025-12-10T02:57:22.474Z" }, + { url = "https://files.pythonhosted.org/packages/57/f7/e19e6eff445bec52dde5bed1ebb162925a8e6f988164f1ae4b3475a73680/numba-0.63.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:0bd4fd820ef7442dcc07da184c3f54bb41d2bdb7b35bacf3448e73d081f730dc", size = 2680954, upload-time = "2025-12-10T02:57:24.145Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6c/1e222edba1e20e6b113912caa9b1665b5809433cbcb042dfd133c6f1fd38/numba-0.63.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:53de693abe4be3bd4dee38e1c55f01c55ff644a6a3696a3670589e6e4c39cde2", size = 3809736, upload-time = "2025-12-10T02:57:25.836Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/590bad11a8b3feeac30a24d01198d46bdb76ad15c70d3a530691ce3cae58/numba-0.63.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:81227821a72a763c3d4ac290abbb4371d855b59fdf85d5af22a47c0e86bf8c7e", size = 3508854, upload-time = "2025-12-10T02:57:27.438Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f5/3800384a24eed1e4d524669cdbc0b9b8a628800bb1e90d7bd676e5f22581/numba-0.63.1-cp313-cp313-win_amd64.whl", hash = "sha256:eb227b07c2ac37b09432a9bda5142047a2d1055646e089d4a240a2643e508102", size = 2750228, upload-time = "2025-12-10T02:57:30.36Z" }, + { url = "https://files.pythonhosted.org/packages/36/2f/53be2aa8a55ee2608ebe1231789cbb217f6ece7f5e1c685d2f0752e95a5b/numba-0.63.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:f180883e5508940cc83de8a8bea37fc6dd20fbe4e5558d4659b8b9bef5ff4731", size = 2681153, upload-time = "2025-12-10T02:57:32.016Z" }, + { url = "https://files.pythonhosted.org/packages/13/91/53e59c86759a0648282368d42ba732c29524a745fd555ed1fb1df83febbe/numba-0.63.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0938764afa82a47c0e895637a6c55547a42c9e1d35cac42285b1fa60a8b02bb", size = 3778718, upload-time = "2025-12-10T02:57:33.764Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0c/2be19eba50b0b7636f6d1f69dfb2825530537708a234ba1ff34afc640138/numba-0.63.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f90a929fa5094e062d4e0368ede1f4497d5e40f800e80aa5222c4734236a2894", size = 3478712, upload-time = "2025-12-10T02:57:35.518Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5f/4d0c9e756732577a52211f31da13a3d943d185f7fb90723f56d79c696caa/numba-0.63.1-cp314-cp314-win_amd64.whl", hash = "sha256:8d6d5ce85f572ed4e1a135dbb8c0114538f9dd0e3657eeb0bb64ab204cbe2a8f", size = 2752161, upload-time = "2025-12-10T02:57:37.12Z" }, +] + +[[package]] +name = "numcodecs" +version = "0.16.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/bd/8a391e7c356366224734efd24da929cc4796fff468bfb179fe1af6548535/numcodecs-0.16.5.tar.gz", hash = "sha256:0d0fb60852f84c0bd9543cc4d2ab9eefd37fc8efcc410acd4777e62a1d300318", size = 6276387, upload-time = "2025-11-21T02:49:48.986Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/cc/55420f3641a67f78392dc0bc5d02cb9eb0a9dcebf2848d1ac77253ca61fa/numcodecs-0.16.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:24e675dc8d1550cd976a99479b87d872cb142632c75cc402fea04c08c4898523", size = 1656287, upload-time = "2025-11-21T02:49:25.755Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6c/86644987505dcb90ba6d627d6989c27bafb0699f9fd00187e06d05ea8594/numcodecs-0.16.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:94ddfa4341d1a3ab99989d13b01b5134abb687d3dab2ead54b450aefe4ad5bd6", size = 1148899, upload-time = "2025-11-21T02:49:26.87Z" }, + { url = "https://files.pythonhosted.org/packages/97/1e/98aaddf272552d9fef1f0296a9939d1487914a239e98678f6b20f8b0a5c8/numcodecs-0.16.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b554ab9ecf69de7ca2b6b5e8bc696bd9747559cb4dd5127bd08d7a28bec59c3a", size = 8534814, upload-time = "2025-11-21T02:49:28.547Z" }, + { url = "https://files.pythonhosted.org/packages/fb/53/78c98ef5c8b2b784453487f3e4d6c017b20747c58b470393e230c78d18e8/numcodecs-0.16.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad1a379a45bd3491deab8ae6548313946744f868c21d5340116977ea3be5b1d6", size = 9173471, upload-time = "2025-11-21T02:49:30.444Z" }, + { url = "https://files.pythonhosted.org/packages/1c/20/2fdec87fc7f8cec950d2b0bea603c12dc9f05b4966dc5924ba5a36a61bf6/numcodecs-0.16.5-cp312-cp312-win_amd64.whl", hash = "sha256:845a9857886ffe4a3172ba1c537ae5bcc01e65068c31cf1fce1a844bd1da050f", size = 801412, upload-time = "2025-11-21T02:49:32.123Z" }, + { url = "https://files.pythonhosted.org/packages/38/38/071ced5a5fd1c85ba0e14ba721b66b053823e5176298c2f707e50bed11d9/numcodecs-0.16.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25be3a516ab677dad890760d357cfe081a371d9c0a2e9a204562318ac5969de3", size = 1654359, upload-time = "2025-11-21T02:49:33.673Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c0/5f84ba7525577c1b9909fc2d06ef11314825fc4ad4378f61d0e4c9883b4a/numcodecs-0.16.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0107e839ef75b854e969cb577e140b1aadb9847893937636582d23a2a4c6ce50", size = 1144237, upload-time = "2025-11-21T02:49:35.294Z" }, + { url = "https://files.pythonhosted.org/packages/0b/00/787ea5f237b8ea7bc67140c99155f9c00b5baf11c49afc5f3bfefa298f95/numcodecs-0.16.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:015a7c859ecc2a06e2a548f64008c0ec3aaecabc26456c2c62f4278d8fc20597", size = 8483064, upload-time = "2025-11-21T02:49:36.454Z" }, + { url = "https://files.pythonhosted.org/packages/c4/e6/d359fdd37498e74d26a167f7a51e54542e642ea47181eb4e643a69a066c3/numcodecs-0.16.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84230b4b9dad2392f2a84242bd6e3e659ac137b5a1ce3571d6965fca673e0903", size = 9126063, upload-time = "2025-11-21T02:49:38.018Z" }, + { url = "https://files.pythonhosted.org/packages/27/72/6663cc0382ddbb866136c255c837bcb96cc7ce5e83562efec55e1b995941/numcodecs-0.16.5-cp313-cp313-win_amd64.whl", hash = "sha256:5088145502ad1ebf677ec47d00eb6f0fd600658217db3e0c070c321c85d6cf3d", size = 799275, upload-time = "2025-11-21T02:49:39.558Z" }, + { url = "https://files.pythonhosted.org/packages/3c/9e/38e7ca8184c958b51f45d56a4aeceb1134ecde2d8bd157efadc98502cc42/numcodecs-0.16.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b05647b8b769e6bc8016e9fd4843c823ce5c9f2337c089fb5c9c4da05e5275de", size = 1654721, upload-time = "2025-11-21T02:49:40.602Z" }, + { url = "https://files.pythonhosted.org/packages/a1/37/260fa42e7b2b08e6e00ad632f8dd620961a60a459426c26cea390f8c68d0/numcodecs-0.16.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3832bd1b5af8bb3e413076b7d93318c8e7d7b68935006b9fa36ca057d1725a8f", size = 1146887, upload-time = "2025-11-21T02:49:41.721Z" }, + { url = "https://files.pythonhosted.org/packages/4e/15/e2e1151b5a8b14a15dfd4bb4abccce7fff7580f39bc34092780088835f3a/numcodecs-0.16.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49f7b7d24f103187f53135bed28bb9f0ed6b2e14c604664726487bb6d7c882e1", size = 8476987, upload-time = "2025-11-21T02:49:43.363Z" }, + { url = "https://files.pythonhosted.org/packages/6d/30/16a57fc4d9fb0ba06c600408bd6634f2f1753c54a7a351c99c5e09b51ee2/numcodecs-0.16.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aec9736d81b70f337d89c4070ee3ffeff113f386fd789492fa152d26a15043e4", size = 9102377, upload-time = "2025-11-21T02:49:45.508Z" }, + { url = "https://files.pythonhosted.org/packages/31/a5/a0425af36c20d55a3ea884db4b4efca25a43bea9214ba69ca7932dd997b4/numcodecs-0.16.5-cp314-cp314-win_amd64.whl", hash = "sha256:b16a14303800e9fb88abc39463ab4706c037647ac17e49e297faa5f7d7dbbf1d", size = 819022, upload-time = "2025-11-21T02:49:47.39Z" }, +] + +[[package]] +name = "numpy" +version = "2.3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873, upload-time = "2025-11-16T22:49:49.84Z" }, + { url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838, upload-time = "2025-11-16T22:49:52.863Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378, upload-time = "2025-11-16T22:49:55.055Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9c/1ca85fb86708724275103b81ec4cf1ac1d08f465368acfc8da7ab545bdae/numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", size = 6628559, upload-time = "2025-11-16T22:49:57.371Z" }, + { url = "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", size = 14250702, upload-time = "2025-11-16T22:49:59.632Z" }, + { url = "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", size = 16606086, upload-time = "2025-11-16T22:50:02.127Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c5/5ad26fbfbe2012e190cc7d5003e4d874b88bb18861d0829edc140a713021/numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", size = 16025985, upload-time = "2025-11-16T22:50:04.536Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fa/dd48e225c46c819288148d9d060b047fd2a6fb1eb37eae25112ee4cb4453/numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", size = 18542976, upload-time = "2025-11-16T22:50:07.557Z" }, + { url = "https://files.pythonhosted.org/packages/05/79/ccbd23a75862d95af03d28b5c6901a1b7da4803181513d52f3b86ed9446e/numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", size = 6285274, upload-time = "2025-11-16T22:50:10.746Z" }, + { url = "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", size = 12782922, upload-time = "2025-11-16T22:50:12.811Z" }, + { url = "https://files.pythonhosted.org/packages/78/a6/aae5cc2ca78c45e64b9ef22f089141d661516856cf7c8a54ba434576900d/numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", size = 10194667, upload-time = "2025-11-16T22:50:16.16Z" }, + { url = "https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251, upload-time = "2025-11-16T22:50:19.013Z" }, + { url = "https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", size = 12254652, upload-time = "2025-11-16T22:50:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/78/da/8c7738060ca9c31b30e9301ee0cf6c5ffdbf889d9593285a1cead337f9a5/numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", size = 5083172, upload-time = "2025-11-16T22:50:24.562Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b4/ee5bb2537fb9430fd2ef30a616c3672b991a4129bb1c7dcc42aa0abbe5d7/numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903", size = 6622990, upload-time = "2025-11-16T22:50:26.47Z" }, + { url = "https://files.pythonhosted.org/packages/95/03/dc0723a013c7d7c19de5ef29e932c3081df1c14ba582b8b86b5de9db7f0f/numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d", size = 14248902, upload-time = "2025-11-16T22:50:28.861Z" }, + { url = "https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017", size = 16597430, upload-time = "2025-11-16T22:50:31.56Z" }, + { url = "https://files.pythonhosted.org/packages/2a/51/c1e29be863588db58175175f057286900b4b3327a1351e706d5e0f8dd679/numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf", size = 16024551, upload-time = "2025-11-16T22:50:34.242Z" }, + { url = "https://files.pythonhosted.org/packages/83/68/8236589d4dbb87253d28259d04d9b814ec0ecce7cb1c7fed29729f4c3a78/numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce", size = 18533275, upload-time = "2025-11-16T22:50:37.651Z" }, + { url = "https://files.pythonhosted.org/packages/40/56/2932d75b6f13465239e3b7b7e511be27f1b8161ca2510854f0b6e521c395/numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", size = 6277637, upload-time = "2025-11-16T22:50:40.11Z" }, + { url = "https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", size = 12779090, upload-time = "2025-11-16T22:50:42.503Z" }, + { url = "https://files.pythonhosted.org/packages/8f/88/3f41e13a44ebd4034ee17baa384acac29ba6a4fcc2aca95f6f08ca0447d1/numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", size = 10194710, upload-time = "2025-11-16T22:50:44.971Z" }, + { url = "https://files.pythonhosted.org/packages/13/cb/71744144e13389d577f867f745b7df2d8489463654a918eea2eeb166dfc9/numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd", size = 16827292, upload-time = "2025-11-16T22:50:47.715Z" }, + { url = "https://files.pythonhosted.org/packages/71/80/ba9dc6f2a4398e7f42b708a7fdc841bb638d353be255655498edbf9a15a8/numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f", size = 12378897, upload-time = "2025-11-16T22:50:51.327Z" }, + { url = "https://files.pythonhosted.org/packages/2e/6d/db2151b9f64264bcceccd51741aa39b50150de9b602d98ecfe7e0c4bff39/numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a", size = 5207391, upload-time = "2025-11-16T22:50:54.542Z" }, + { url = "https://files.pythonhosted.org/packages/80/ae/429bacace5ccad48a14c4ae5332f6aa8ab9f69524193511d60ccdfdc65fa/numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139", size = 6721275, upload-time = "2025-11-16T22:50:56.794Z" }, + { url = "https://files.pythonhosted.org/packages/74/5b/1919abf32d8722646a38cd527bc3771eb229a32724ee6ba340ead9b92249/numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e", size = 14306855, upload-time = "2025-11-16T22:50:59.208Z" }, + { url = "https://files.pythonhosted.org/packages/a5/87/6831980559434973bebc30cd9c1f21e541a0f2b0c280d43d3afd909b66d0/numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9", size = 16657359, upload-time = "2025-11-16T22:51:01.991Z" }, + { url = "https://files.pythonhosted.org/packages/dd/91/c797f544491ee99fd00495f12ebb7802c440c1915811d72ac5b4479a3356/numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946", size = 16093374, upload-time = "2025-11-16T22:51:05.291Z" }, + { url = "https://files.pythonhosted.org/packages/74/a6/54da03253afcbe7a72785ec4da9c69fb7a17710141ff9ac5fcb2e32dbe64/numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1", size = 18594587, upload-time = "2025-11-16T22:51:08.585Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940, upload-time = "2025-11-16T22:51:11.541Z" }, + { url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341, upload-time = "2025-11-16T22:51:14.312Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507, upload-time = "2025-11-16T22:51:16.846Z" }, + { url = "https://files.pythonhosted.org/packages/ba/97/1a914559c19e32d6b2e233cf9a6a114e67c856d35b1d6babca571a3e880f/numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82", size = 16735706, upload-time = "2025-11-16T22:51:19.558Z" }, + { url = "https://files.pythonhosted.org/packages/57/d4/51233b1c1b13ecd796311216ae417796b88b0616cfd8a33ae4536330748a/numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0", size = 12264507, upload-time = "2025-11-16T22:51:22.492Z" }, + { url = "https://files.pythonhosted.org/packages/45/98/2fe46c5c2675b8306d0b4a3ec3494273e93e1226a490f766e84298576956/numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63", size = 5093049, upload-time = "2025-11-16T22:51:25.171Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0e/0698378989bb0ac5f1660c81c78ab1fe5476c1a521ca9ee9d0710ce54099/numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9", size = 6626603, upload-time = "2025-11-16T22:51:27Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a6/9ca0eecc489640615642a6cbc0ca9e10df70df38c4d43f5a928ff18d8827/numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b", size = 14262696, upload-time = "2025-11-16T22:51:29.402Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f6/07ec185b90ec9d7217a00eeeed7383b73d7e709dae2a9a021b051542a708/numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520", size = 16597350, upload-time = "2025-11-16T22:51:32.167Z" }, + { url = "https://files.pythonhosted.org/packages/75/37/164071d1dde6a1a84c9b8e5b414fa127981bad47adf3a6b7e23917e52190/numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c", size = 16040190, upload-time = "2025-11-16T22:51:35.403Z" }, + { url = "https://files.pythonhosted.org/packages/08/3c/f18b82a406b04859eb026d204e4e1773eb41c5be58410f41ffa511d114ae/numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8", size = 18536749, upload-time = "2025-11-16T22:51:39.698Z" }, + { url = "https://files.pythonhosted.org/packages/40/79/f82f572bf44cf0023a2fe8588768e23e1592585020d638999f15158609e1/numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248", size = 6335432, upload-time = "2025-11-16T22:51:42.476Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2e/235b4d96619931192c91660805e5e49242389742a7a82c27665021db690c/numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e", size = 12919388, upload-time = "2025-11-16T22:51:45.275Z" }, + { url = "https://files.pythonhosted.org/packages/07/2b/29fd75ce45d22a39c61aad74f3d718e7ab67ccf839ca8b60866054eb15f8/numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2", size = 10476651, upload-time = "2025-11-16T22:51:47.749Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/f6a721234ebd4d87084cfa68d081bcba2f5cfe1974f7de4e0e8b9b2a2ba1/numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41", size = 16834503, upload-time = "2025-11-16T22:51:50.443Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1c/baf7ffdc3af9c356e1c135e57ab7cf8d247931b9554f55c467efe2c69eff/numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad", size = 12381612, upload-time = "2025-11-16T22:51:53.609Z" }, + { url = "https://files.pythonhosted.org/packages/74/91/f7f0295151407ddc9ba34e699013c32c3c91944f9b35fcf9281163dc1468/numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39", size = 5210042, upload-time = "2025-11-16T22:51:56.213Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3b/78aebf345104ec50dd50a4d06ddeb46a9ff5261c33bcc58b1c4f12f85ec2/numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20", size = 6724502, upload-time = "2025-11-16T22:51:58.584Z" }, + { url = "https://files.pythonhosted.org/packages/02/c6/7c34b528740512e57ef1b7c8337ab0b4f0bddf34c723b8996c675bc2bc91/numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52", size = 14308962, upload-time = "2025-11-16T22:52:01.698Z" }, + { url = "https://files.pythonhosted.org/packages/80/35/09d433c5262bc32d725bafc619e095b6a6651caf94027a03da624146f655/numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b", size = 16655054, upload-time = "2025-11-16T22:52:04.267Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ab/6a7b259703c09a88804fa2430b43d6457b692378f6b74b356155283566ac/numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3", size = 16091613, upload-time = "2025-11-16T22:52:08.651Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/330da2071e8771e60d1038166ff9d73f29da37b01ec3eb43cb1427464e10/numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227", size = 18591147, upload-time = "2025-11-16T22:52:11.453Z" }, + { url = "https://files.pythonhosted.org/packages/51/41/851c4b4082402d9ea860c3626db5d5df47164a712cb23b54be028b184c1c/numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5", size = 6479806, upload-time = "2025-11-16T22:52:14.641Z" }, + { url = "https://files.pythonhosted.org/packages/90/30/d48bde1dfd93332fa557cff1972fbc039e055a52021fbef4c2c4b1eefd17/numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf", size = 13105760, upload-time = "2025-11-16T22:52:17.975Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459, upload-time = "2025-11-16T22:52:20.55Z" }, +] + +[[package]] +name = "ome-zarr" +version = "0.12.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "dask" }, + { name = "fsspec", extra = ["s3"] }, + { name = "numpy" }, + { name = "requests" }, + { name = "scikit-image" }, + { name = "toolz" }, + { name = "zarr" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a4/44/647843d872aa136609e805c06e9f9b2cdcb6e2a58ec485322311dec7b64d/ome_zarr-0.12.2.tar.gz", hash = "sha256:834e801e9aa4b870bed3dde2dc2a3ad7f388f1a13ffa6b3d7aade90691b9de64", size = 69891, upload-time = "2025-08-22T08:57:13.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/21/59baa90924b815b70f88045f0b206b7eab0b68b461c0192692486b516ab7/ome_zarr-0.12.2-py3-none-any.whl", hash = "sha256:655fe1b11ca01148603f9931a5b0af31207dfc03a3a35f9b0ab8639790282bbd", size = 41410, upload-time = "2025-08-22T08:57:12.44Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, +] + +[[package]] +name = "param" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/bb/ffd1606c28a957fb6444ed3edefe41373cdd7b3e001630b07e3a53a6bea3/param-2.3.1.tar.gz", hash = "sha256:84e59fc3a9bfb0e4c8100eb92d5be529deea3ec9c1f0881a0068c5caf31f21f3", size = 201772, upload-time = "2025-11-25T15:35:54.842Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/da/9d476e9aadfa854719f3cb917e3f7a170a657a182d8d1d6e546594a4872b/param-2.3.1-py3-none-any.whl", hash = "sha256:886b19031438719bbecfd15044dcdd9ed3cb9edb199191294f75600c7081d163", size = 139818, upload-time = "2025-11-25T15:35:53.556Z" }, +] + +[[package]] +name = "partd" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "locket" }, + { name = "toolz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/3a/3f06f34820a31257ddcabdfafc2672c5816be79c7e353b02c1f318daa7d4/partd-1.4.2.tar.gz", hash = "sha256:d022c33afbdc8405c226621b015e8067888173d85f7f5ecebb3cafed9a20f02c", size = 21029, upload-time = "2024-05-06T19:51:41.945Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl", hash = "sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f", size = 18905, upload-time = "2024-05-06T19:51:39.271Z" }, +] + +[[package]] +name = "pathlib-abc" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/cb/448649d7f25d228bf0be3a04590ab7afa77f15e056f8fa976ed05ec9a78f/pathlib_abc-0.5.2.tar.gz", hash = "sha256:fcd56f147234645e2c59c7ae22808b34c364bb231f685ddd9f96885aed78a94c", size = 33342, upload-time = "2025-10-10T18:37:20.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/29/c028a0731e202035f0e2e0bfbf1a3e46ad6c628cbb17f6f1cc9eea5d9ff1/pathlib_abc-0.5.2-py3-none-any.whl", hash = "sha256:4c9d94cf1b23af417ce7c0417b43333b06a106c01000b286c99de230d95eefbb", size = 19070, upload-time = "2025-10-10T18:37:19.437Z" }, +] + +[[package]] +name = "pillow" +version = "12.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/31/dc53fe21a2f2996e1b7d92bf671cdb157079385183ef7c1ae08b485db510/pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b", size = 5262642, upload-time = "2026-01-02T09:11:10.138Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c1/10e45ac9cc79419cedf5121b42dcca5a50ad2b601fa080f58c22fb27626e/pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551", size = 4657464, upload-time = "2026-01-02T09:11:12.319Z" }, + { url = "https://files.pythonhosted.org/packages/ad/26/7b82c0ab7ef40ebede7a97c72d473bda5950f609f8e0c77b04af574a0ddb/pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208", size = 6234878, upload-time = "2026-01-02T09:11:14.096Z" }, + { url = "https://files.pythonhosted.org/packages/76/25/27abc9792615b5e886ca9411ba6637b675f1b77af3104710ac7353fe5605/pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5", size = 8044868, upload-time = "2026-01-02T09:11:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ea/f200a4c36d836100e7bc738fc48cd963d3ba6372ebc8298a889e0cfc3359/pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661", size = 6349468, upload-time = "2026-01-02T09:11:17.631Z" }, + { url = "https://files.pythonhosted.org/packages/11/8f/48d0b77ab2200374c66d344459b8958c86693be99526450e7aee714e03e4/pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17", size = 7041518, upload-time = "2026-01-02T09:11:19.389Z" }, + { url = "https://files.pythonhosted.org/packages/1d/23/c281182eb986b5d31f0a76d2a2c8cd41722d6fb8ed07521e802f9bba52de/pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670", size = 6462829, upload-time = "2026-01-02T09:11:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/25/ef/7018273e0faac099d7b00982abdcc39142ae6f3bd9ceb06de09779c4a9d6/pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616", size = 7166756, upload-time = "2026-01-02T09:11:23.559Z" }, + { url = "https://files.pythonhosted.org/packages/8f/c8/993d4b7ab2e341fe02ceef9576afcf5830cdec640be2ac5bee1820d693d4/pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7", size = 6328770, upload-time = "2026-01-02T09:11:25.661Z" }, + { url = "https://files.pythonhosted.org/packages/a7/87/90b358775a3f02765d87655237229ba64a997b87efa8ccaca7dd3e36e7a7/pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d", size = 7033406, upload-time = "2026-01-02T09:11:27.474Z" }, + { url = "https://files.pythonhosted.org/packages/5d/cf/881b457eccacac9e5b2ddd97d5071fb6d668307c57cbf4e3b5278e06e536/pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c", size = 2452612, upload-time = "2026-01-02T09:11:29.309Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c7/2530a4aa28248623e9d7f27316b42e27c32ec410f695929696f2e0e4a778/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1", size = 4062543, upload-time = "2026-01-02T09:11:31.566Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1f/40b8eae823dc1519b87d53c30ed9ef085506b05281d313031755c1705f73/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179", size = 4138373, upload-time = "2026-01-02T09:11:33.367Z" }, + { url = "https://files.pythonhosted.org/packages/d4/77/6fa60634cf06e52139fd0e89e5bbf055e8166c691c42fb162818b7fda31d/pillow-12.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0", size = 3601241, upload-time = "2026-01-02T09:11:35.011Z" }, + { url = "https://files.pythonhosted.org/packages/4f/bf/28ab865de622e14b747f0cd7877510848252d950e43002e224fb1c9ababf/pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587", size = 5262410, upload-time = "2026-01-02T09:11:36.682Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/583420a1b55e715937a85bd48c5c0991598247a1fd2eb5423188e765ea02/pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac", size = 4657312, upload-time = "2026-01-02T09:11:38.535Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fd/f5a0896839762885b3376ff04878f86ab2b097c2f9a9cdccf4eda8ba8dc0/pillow-12.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b", size = 6232605, upload-time = "2026-01-02T09:11:40.602Z" }, + { url = "https://files.pythonhosted.org/packages/98/aa/938a09d127ac1e70e6ed467bd03834350b33ef646b31edb7452d5de43792/pillow-12.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea", size = 8041617, upload-time = "2026-01-02T09:11:42.721Z" }, + { url = "https://files.pythonhosted.org/packages/17/e8/538b24cb426ac0186e03f80f78bc8dc7246c667f58b540bdd57c71c9f79d/pillow-12.1.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c", size = 6346509, upload-time = "2026-01-02T09:11:44.955Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/632e58ec89a32738cabfd9ec418f0e9898a2b4719afc581f07c04a05e3c9/pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc", size = 7038117, upload-time = "2026-01-02T09:11:46.736Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a2/d40308cf86eada842ca1f3ffa45d0ca0df7e4ab33c83f81e73f5eaed136d/pillow-12.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644", size = 6460151, upload-time = "2026-01-02T09:11:48.625Z" }, + { url = "https://files.pythonhosted.org/packages/f1/88/f5b058ad6453a085c5266660a1417bdad590199da1b32fb4efcff9d33b05/pillow-12.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c", size = 7164534, upload-time = "2026-01-02T09:11:50.445Z" }, + { url = "https://files.pythonhosted.org/packages/19/ce/c17334caea1db789163b5d855a5735e47995b0b5dc8745e9a3605d5f24c0/pillow-12.1.0-cp313-cp313-win32.whl", hash = "sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171", size = 6332551, upload-time = "2026-01-02T09:11:52.234Z" }, + { url = "https://files.pythonhosted.org/packages/e5/07/74a9d941fa45c90a0d9465098fe1ec85de3e2afbdc15cc4766622d516056/pillow-12.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a", size = 7040087, upload-time = "2026-01-02T09:11:54.822Z" }, + { url = "https://files.pythonhosted.org/packages/88/09/c99950c075a0e9053d8e880595926302575bc742b1b47fe1bbcc8d388d50/pillow-12.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45", size = 2452470, upload-time = "2026-01-02T09:11:56.522Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ba/970b7d85ba01f348dee4d65412476321d40ee04dcb51cd3735b9dc94eb58/pillow-12.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d", size = 5264816, upload-time = "2026-01-02T09:11:58.227Z" }, + { url = "https://files.pythonhosted.org/packages/10/60/650f2fb55fdba7a510d836202aa52f0baac633e50ab1cf18415d332188fb/pillow-12.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0", size = 4660472, upload-time = "2026-01-02T09:12:00.798Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/5273a99478956a099d533c4f46cbaa19fd69d606624f4334b85e50987a08/pillow-12.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554", size = 6268974, upload-time = "2026-01-02T09:12:02.572Z" }, + { url = "https://files.pythonhosted.org/packages/b4/26/0bf714bc2e73d5267887d47931d53c4ceeceea6978148ed2ab2a4e6463c4/pillow-12.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e", size = 8073070, upload-time = "2026-01-02T09:12:04.75Z" }, + { url = "https://files.pythonhosted.org/packages/43/cf/1ea826200de111a9d65724c54f927f3111dc5ae297f294b370a670c17786/pillow-12.1.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82", size = 6380176, upload-time = "2026-01-02T09:12:06.626Z" }, + { url = "https://files.pythonhosted.org/packages/03/e0/7938dd2b2013373fd85d96e0f38d62b7a5a262af21ac274250c7ca7847c9/pillow-12.1.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4", size = 7067061, upload-time = "2026-01-02T09:12:08.624Z" }, + { url = "https://files.pythonhosted.org/packages/86/ad/a2aa97d37272a929a98437a8c0ac37b3cf012f4f8721e1bd5154699b2518/pillow-12.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0", size = 6491824, upload-time = "2026-01-02T09:12:10.488Z" }, + { url = "https://files.pythonhosted.org/packages/a4/44/80e46611b288d51b115826f136fb3465653c28f491068a72d3da49b54cd4/pillow-12.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b", size = 7190911, upload-time = "2026-01-02T09:12:12.772Z" }, + { url = "https://files.pythonhosted.org/packages/86/77/eacc62356b4cf81abe99ff9dbc7402750044aed02cfd6a503f7c6fc11f3e/pillow-12.1.0-cp313-cp313t-win32.whl", hash = "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65", size = 6336445, upload-time = "2026-01-02T09:12:14.775Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3c/57d81d0b74d218706dafccb87a87ea44262c43eef98eb3b164fd000e0491/pillow-12.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0", size = 7045354, upload-time = "2026-01-02T09:12:16.599Z" }, + { url = "https://files.pythonhosted.org/packages/ac/82/8b9b97bba2e3576a340f93b044a3a3a09841170ab4c1eb0d5c93469fd32f/pillow-12.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8", size = 2454547, upload-time = "2026-01-02T09:12:18.704Z" }, + { url = "https://files.pythonhosted.org/packages/8c/87/bdf971d8bbcf80a348cc3bacfcb239f5882100fe80534b0ce67a784181d8/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91", size = 4062533, upload-time = "2026-01-02T09:12:20.791Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4f/5eb37a681c68d605eb7034c004875c81f86ec9ef51f5be4a63eadd58859a/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796", size = 4138546, upload-time = "2026-01-02T09:12:23.664Z" }, + { url = "https://files.pythonhosted.org/packages/11/6d/19a95acb2edbace40dcd582d077b991646b7083c41b98da4ed7555b59733/pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd", size = 3601163, upload-time = "2026-01-02T09:12:26.338Z" }, + { url = "https://files.pythonhosted.org/packages/fc/36/2b8138e51cb42e4cc39c3297713455548be855a50558c3ac2beebdc251dd/pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13", size = 5266086, upload-time = "2026-01-02T09:12:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/649056e4d22e1caa90816bf99cef0884aed607ed38075bd75f091a607a38/pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e", size = 4657344, upload-time = "2026-01-02T09:12:31.117Z" }, + { url = "https://files.pythonhosted.org/packages/6c/6b/c5742cea0f1ade0cd61485dc3d81f05261fc2276f537fbdc00802de56779/pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643", size = 6232114, upload-time = "2026-01-02T09:12:32.936Z" }, + { url = "https://files.pythonhosted.org/packages/bf/8f/9f521268ce22d63991601aafd3d48d5ff7280a246a1ef62d626d67b44064/pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5", size = 8042708, upload-time = "2026-01-02T09:12:34.78Z" }, + { url = "https://files.pythonhosted.org/packages/1a/eb/257f38542893f021502a1bbe0c2e883c90b5cff26cc33b1584a841a06d30/pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de", size = 6347762, upload-time = "2026-01-02T09:12:36.748Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5a/8ba375025701c09b309e8d5163c5a4ce0102fa86bbf8800eb0d7ac87bc51/pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9", size = 7039265, upload-time = "2026-01-02T09:12:39.082Z" }, + { url = "https://files.pythonhosted.org/packages/cf/dc/cf5e4cdb3db533f539e88a7bbf9f190c64ab8a08a9bc7a4ccf55067872e4/pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a", size = 6462341, upload-time = "2026-01-02T09:12:40.946Z" }, + { url = "https://files.pythonhosted.org/packages/d0/47/0291a25ac9550677e22eda48510cfc4fa4b2ef0396448b7fbdc0a6946309/pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a", size = 7165395, upload-time = "2026-01-02T09:12:42.706Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4c/e005a59393ec4d9416be06e6b45820403bb946a778e39ecec62f5b2b991e/pillow-12.1.0-cp314-cp314-win32.whl", hash = "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030", size = 6431413, upload-time = "2026-01-02T09:12:44.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/af/f23697f587ac5f9095d67e31b81c95c0249cd461a9798a061ed6709b09b5/pillow-12.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94", size = 7176779, upload-time = "2026-01-02T09:12:46.727Z" }, + { url = "https://files.pythonhosted.org/packages/b3/36/6a51abf8599232f3e9afbd16d52829376a68909fe14efe29084445db4b73/pillow-12.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4", size = 2543105, upload-time = "2026-01-02T09:12:49.243Z" }, + { url = "https://files.pythonhosted.org/packages/82/54/2e1dd20c8749ff225080d6ba465a0cab4387f5db0d1c5fb1439e2d99923f/pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2", size = 5268571, upload-time = "2026-01-02T09:12:51.11Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/571163a5ef86ec0cf30d265ac2a70ae6fc9e28413d1dc94fa37fae6bda89/pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61", size = 4660426, upload-time = "2026-01-02T09:12:52.865Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e1/53ee5163f794aef1bf84243f755ee6897a92c708505350dd1923f4afec48/pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51", size = 6269908, upload-time = "2026-01-02T09:12:54.884Z" }, + { url = "https://files.pythonhosted.org/packages/bc/0b/b4b4106ff0ee1afa1dc599fde6ab230417f800279745124f6c50bcffed8e/pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc", size = 8074733, upload-time = "2026-01-02T09:12:56.802Z" }, + { url = "https://files.pythonhosted.org/packages/19/9f/80b411cbac4a732439e629a26ad3ef11907a8c7fc5377b7602f04f6fe4e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14", size = 6381431, upload-time = "2026-01-02T09:12:58.823Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b7/d65c45db463b66ecb6abc17c6ba6917a911202a07662247e1355ce1789e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8", size = 7068529, upload-time = "2026-01-02T09:13:00.885Z" }, + { url = "https://files.pythonhosted.org/packages/50/96/dfd4cd726b4a45ae6e3c669fc9e49deb2241312605d33aba50499e9d9bd1/pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924", size = 6492981, upload-time = "2026-01-02T09:13:03.314Z" }, + { url = "https://files.pythonhosted.org/packages/4d/1c/b5dc52cf713ae46033359c5ca920444f18a6359ce1020dd3e9c553ea5bc6/pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef", size = 7191878, upload-time = "2026-01-02T09:13:05.276Z" }, + { url = "https://files.pythonhosted.org/packages/53/26/c4188248bd5edaf543864fe4834aebe9c9cb4968b6f573ce014cc42d0720/pillow-12.1.0-cp314-cp314t-win32.whl", hash = "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988", size = 6438703, upload-time = "2026-01-02T09:13:07.491Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0e/69ed296de8ea05cb03ee139cee600f424ca166e632567b2d66727f08c7ed/pillow-12.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6", size = 7182927, upload-time = "2026-01-02T09:13:09.841Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f5/68334c015eed9b5cff77814258717dec591ded209ab5b6fb70e2ae873d1d/pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831", size = 2545104, upload-time = "2026-01-02T09:13:12.068Z" }, +] + +[[package]] +name = "pims" +version = "0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "imageio" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "slicerator" }, + { name = "tifffile" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/02/5bf3639f5b77e9b183011c08541c5039ba3d04f5316c70312b48a8e003a9/pims-0.7.tar.gz", hash = "sha256:55907a4c301256086d2aa4e34a5361b9109f24e375c2071e1117b9491e82946b", size = 87779, upload-time = "2024-06-10T19:20:42.842Z" } + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, +] + +[[package]] +name = "pooch" +version = "1.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "platformdirs" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/77/b3d3e00c696c16cf99af81ef7b1f5fe73bd2a307abca41bd7605429fe6e5/pooch-1.8.2.tar.gz", hash = "sha256:76561f0de68a01da4df6af38e9955c4c9d1a5c90da73f7e40276a5728ec83d10", size = 59353, upload-time = "2024-06-06T16:53:46.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/87/77cc11c7a9ea9fd05503def69e3d18605852cd0d4b0d3b8f15bbeb3ef1d1/pooch-1.8.2-py3-none-any.whl", hash = "sha256:3529a57096f7198778a5ceefd5ac3ef0e4d06a6ddaf9fc2d609b806f25302c47", size = 64574, upload-time = "2024-06-06T16:53:44.343Z" }, +] + +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "pyarrow" +version = "23.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/33/ffd9c3eb087fa41dd79c3cf20c4c0ae3cdb877c4f8e1107a446006344924/pyarrow-23.0.0.tar.gz", hash = "sha256:180e3150e7edfcd182d3d9afba72f7cf19839a497cc76555a8dce998a8f67615", size = 1167185, upload-time = "2026-01-18T16:19:42.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/bd/c861d020831ee57609b73ea721a617985ece817684dc82415b0bc3e03ac3/pyarrow-23.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5961a9f646c232697c24f54d3419e69b4261ba8a8b66b0ac54a1851faffcbab8", size = 34189116, upload-time = "2026-01-18T16:15:28.054Z" }, + { url = "https://files.pythonhosted.org/packages/8c/23/7725ad6cdcbaf6346221391e7b3eecd113684c805b0a95f32014e6fa0736/pyarrow-23.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:632b3e7c3d232f41d64e1a4a043fb82d44f8a349f339a1188c6a0dd9d2d47d8a", size = 35803831, upload-time = "2026-01-18T16:15:33.798Z" }, + { url = "https://files.pythonhosted.org/packages/57/06/684a421543455cdc2944d6a0c2cc3425b028a4c6b90e34b35580c4899743/pyarrow-23.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:76242c846db1411f1d6c2cc3823be6b86b40567ee24493344f8226ba34a81333", size = 44436452, upload-time = "2026-01-18T16:15:41.598Z" }, + { url = "https://files.pythonhosted.org/packages/c6/6f/8f9eb40c2328d66e8b097777ddcf38494115ff9f1b5bc9754ba46991191e/pyarrow-23.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b73519f8b52ae28127000986bf228fda781e81d3095cd2d3ece76eb5cf760e1b", size = 47557396, upload-time = "2026-01-18T16:15:51.252Z" }, + { url = "https://files.pythonhosted.org/packages/10/6e/f08075f1472e5159553501fde2cc7bc6700944bdabe49a03f8a035ee6ccd/pyarrow-23.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:068701f6823449b1b6469120f399a1239766b117d211c5d2519d4ed5861f75de", size = 48147129, upload-time = "2026-01-18T16:16:00.299Z" }, + { url = "https://files.pythonhosted.org/packages/7d/82/d5a680cd507deed62d141cc7f07f7944a6766fc51019f7f118e4d8ad0fb8/pyarrow-23.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1801ba947015d10e23bca9dd6ef5d0e9064a81569a89b6e9a63b59224fd060df", size = 50596642, upload-time = "2026-01-18T16:16:08.502Z" }, + { url = "https://files.pythonhosted.org/packages/a9/26/4f29c61b3dce9fa7780303b86895ec6a0917c9af927101daaaf118fbe462/pyarrow-23.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:52265266201ec25b6839bf6bd4ea918ca6d50f31d13e1cf200b4261cd11dc25c", size = 27660628, upload-time = "2026-01-18T16:16:15.28Z" }, + { url = "https://files.pythonhosted.org/packages/66/34/564db447d083ec7ff93e0a883a597d2f214e552823bfc178a2d0b1f2c257/pyarrow-23.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:ad96a597547af7827342ffb3c503c8316e5043bb09b47a84885ce39394c96e00", size = 34184630, upload-time = "2026-01-18T16:16:22.141Z" }, + { url = "https://files.pythonhosted.org/packages/aa/3a/3999daebcb5e6119690c92a621c4d78eef2ffba7a0a1b56386d2875fcd77/pyarrow-23.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:b9edf990df77c2901e79608f08c13fbde60202334a4fcadb15c1f57bf7afee43", size = 35796820, upload-time = "2026-01-18T16:16:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ee/39195233056c6a8d0976d7d1ac1cd4fe21fb0ec534eca76bc23ef3f60e11/pyarrow-23.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:36d1b5bc6ddcaff0083ceec7e2561ed61a51f49cce8be079ee8ed406acb6fdef", size = 44438735, upload-time = "2026-01-18T16:16:38.79Z" }, + { url = "https://files.pythonhosted.org/packages/2c/41/6a7328ee493527e7afc0c88d105ecca69a3580e29f2faaeac29308369fd7/pyarrow-23.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4292b889cd224f403304ddda8b63a36e60f92911f89927ec8d98021845ea21be", size = 47557263, upload-time = "2026-01-18T16:16:46.248Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ee/34e95b21ee84db494eae60083ddb4383477b31fb1fd19fd866d794881696/pyarrow-23.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dfd9e133e60eaa847fd80530a1b89a052f09f695d0b9c34c235ea6b2e0924cf7", size = 48153529, upload-time = "2026-01-18T16:16:53.412Z" }, + { url = "https://files.pythonhosted.org/packages/52/88/8a8d83cea30f4563efa1b7bf51d241331ee5cd1b185a7e063f5634eca415/pyarrow-23.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832141cc09fac6aab1cd3719951d23301396968de87080c57c9a7634e0ecd068", size = 50598851, upload-time = "2026-01-18T16:17:01.133Z" }, + { url = "https://files.pythonhosted.org/packages/c6/4c/2929c4be88723ba025e7b3453047dc67e491c9422965c141d24bab6b5962/pyarrow-23.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:7a7d067c9a88faca655c71bcc30ee2782038d59c802d57950826a07f60d83c4c", size = 27577747, upload-time = "2026-01-18T16:18:02.413Z" }, + { url = "https://files.pythonhosted.org/packages/64/52/564a61b0b82d72bd68ec3aef1adda1e3eba776f89134b9ebcb5af4b13cb6/pyarrow-23.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:ce9486e0535a843cf85d990e2ec5820a47918235183a5c7b8b97ed7e92c2d47d", size = 34446038, upload-time = "2026-01-18T16:17:07.861Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c9/232d4f9855fd1de0067c8a7808a363230d223c83aeee75e0fe6eab851ba9/pyarrow-23.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:075c29aeaa685fd1182992a9ed2499c66f084ee54eea47da3eb76e125e06064c", size = 35921142, upload-time = "2026-01-18T16:17:15.401Z" }, + { url = "https://files.pythonhosted.org/packages/96/f2/60af606a3748367b906bb82d41f0032e059f075444445d47e32a7ff1df62/pyarrow-23.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:799965a5379589510d888be3094c2296efd186a17ca1cef5b77703d4d5121f53", size = 44490374, upload-time = "2026-01-18T16:17:23.93Z" }, + { url = "https://files.pythonhosted.org/packages/ff/2d/7731543050a678ea3a413955a2d5d80d2a642f270aa57a3cb7d5a86e3f46/pyarrow-23.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ef7cac8fe6fccd8b9e7617bfac785b0371a7fe26af59463074e4882747145d40", size = 47527896, upload-time = "2026-01-18T16:17:33.393Z" }, + { url = "https://files.pythonhosted.org/packages/5a/90/f3342553b7ac9879413aed46500f1637296f3c8222107523a43a1c08b42a/pyarrow-23.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15a414f710dc927132dd67c361f78c194447479555af57317066ee5116b90e9e", size = 48210401, upload-time = "2026-01-18T16:17:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/f3/da/9862ade205ecc46c172b6ce5038a74b5151c7401e36255f15975a45878b2/pyarrow-23.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e0d2e6915eca7d786be6a77bf227fbc06d825a75b5b5fe9bcbef121dec32685", size = 50579677, upload-time = "2026-01-18T16:17:50.241Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4c/f11f371f5d4740a5dafc2e11c76bcf42d03dfdb2d68696da97de420b6963/pyarrow-23.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:4b317ea6e800b5704e5e5929acb6e2dc13e9276b708ea97a39eb8b345aa2658b", size = 27631889, upload-time = "2026-01-18T16:17:56.55Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/15aec78bcf43a0c004067bd33eb5352836a29a49db8581fc56f2b6ca88b7/pyarrow-23.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:20b187ed9550d233a872074159f765f52f9d92973191cd4b93f293a19efbe377", size = 34213265, upload-time = "2026-01-18T16:18:07.904Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/deb2c594bbba41c37c5d9aa82f510376998352aa69dfcb886cb4b18ad80f/pyarrow-23.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:18ec84e839b493c3886b9b5e06861962ab4adfaeb79b81c76afbd8d84c7d5fda", size = 35819211, upload-time = "2026-01-18T16:18:13.94Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e5/ee82af693cb7b5b2b74f6524cdfede0e6ace779d7720ebca24d68b57c36b/pyarrow-23.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:e438dd3f33894e34fd02b26bd12a32d30d006f5852315f611aa4add6c7fab4bc", size = 44502313, upload-time = "2026-01-18T16:18:20.367Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/95c61ad82236495f3c31987e85135926ba3ec7f3819296b70a68d8066b49/pyarrow-23.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:a244279f240c81f135631be91146d7fa0e9e840e1dfed2aba8483eba25cd98e6", size = 47585886, upload-time = "2026-01-18T16:18:27.544Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6e/a72d901f305201802f016d015de1e05def7706fff68a1dedefef5dc7eff7/pyarrow-23.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c4692e83e42438dba512a570c6eaa42be2f8b6c0f492aea27dec54bdc495103a", size = 48207055, upload-time = "2026-01-18T16:18:35.425Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e5/5de029c537630ca18828db45c30e2a78da03675a70ac6c3528203c416fe3/pyarrow-23.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae7f30f898dfe44ea69654a35c93e8da4cef6606dc4c72394068fd95f8e9f54a", size = 50619812, upload-time = "2026-01-18T16:18:43.553Z" }, + { url = "https://files.pythonhosted.org/packages/59/8d/2af846cd2412e67a087f5bda4a8e23dfd4ebd570f777db2e8686615dafc1/pyarrow-23.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:5b86bb649e4112fb0614294b7d0a175c7513738876b89655605ebb87c804f861", size = 28263851, upload-time = "2026-01-18T16:19:38.567Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7f/caab863e587041156f6786c52e64151b7386742c8c27140f637176e9230e/pyarrow-23.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:ebc017d765d71d80a3f8584ca0566b53e40464586585ac64176115baa0ada7d3", size = 34463240, upload-time = "2026-01-18T16:18:49.755Z" }, + { url = "https://files.pythonhosted.org/packages/c9/fa/3a5b8c86c958e83622b40865e11af0857c48ec763c11d472c87cd518283d/pyarrow-23.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:0800cc58a6d17d159df823f87ad66cefebf105b982493d4bad03ee7fab84b993", size = 35935712, upload-time = "2026-01-18T16:18:55.626Z" }, + { url = "https://files.pythonhosted.org/packages/c5/08/17a62078fc1a53decb34a9aa79cf9009efc74d63d2422e5ade9fed2f99e3/pyarrow-23.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:3a7c68c722da9bb5b0f8c10e3eae71d9825a4b429b40b32709df5d1fa55beb3d", size = 44503523, upload-time = "2026-01-18T16:19:03.958Z" }, + { url = "https://files.pythonhosted.org/packages/cc/70/84d45c74341e798aae0323d33b7c39194e23b1abc439ceaf60a68a7a969a/pyarrow-23.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:bd5556c24622df90551063ea41f559b714aa63ca953db884cfb958559087a14e", size = 47542490, upload-time = "2026-01-18T16:19:11.208Z" }, + { url = "https://files.pythonhosted.org/packages/61/d9/d1274b0e6f19e235de17441e53224f4716574b2ca837022d55702f24d71d/pyarrow-23.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54810f6e6afc4ffee7c2e0051b61722fbea9a4961b46192dcfae8ea12fa09059", size = 48233605, upload-time = "2026-01-18T16:19:19.544Z" }, + { url = "https://files.pythonhosted.org/packages/39/07/e4e2d568cb57543d84482f61e510732820cddb0f47c4bb7df629abfed852/pyarrow-23.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:14de7d48052cf4b0ed174533eafa3cfe0711b8076ad70bede32cf59f744f0d7c", size = 50603979, upload-time = "2026-01-18T16:19:26.717Z" }, + { url = "https://files.pythonhosted.org/packages/72/9c/47693463894b610f8439b2e970b82ef81e9599c757bf2049365e40ff963c/pyarrow-23.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:427deac1f535830a744a4f04a6ac183a64fcac4341b3f618e693c41b7b98d2b0", size = 28338905, upload-time = "2026-01-18T16:19:32.93Z" }, +] + +[[package]] +name = "pyct" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "param" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/c3/78eeacf7cbf478db6bd41de0ee2221cc6769d81e0c10f6c1616c82a25e06/pyct-0.6.0.tar.gz", hash = "sha256:d4e513b2cf35b6165605ae5fce5f2a49985bc67473c579de85134b8c71d374a8", size = 14586, upload-time = "2025-09-26T08:26:19.987Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/b2/23f4032cd1c9744aa8e9ecda43cd4d755fcb209f7f40fae035248f31a679/pyct-0.6.0-py3-none-any.whl", hash = "sha256:cfaded7289fca72ddf6579b81459e3ec8db323a508e61c49aa318ee3cd6ff160", size = 16630, upload-time = "2025-09-26T08:26:19.092Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyogrio" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "numpy" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/d4/12f86b1ed09721363da4c09622464b604c851a9223fc0c6b393fb2012208/pyogrio-0.12.1.tar.gz", hash = "sha256:e548ab705bb3e5383693717de1e6c76da97f3762ab92522cb310f93128a75ff1", size = 303289, upload-time = "2025-11-28T19:04:53.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/e0/656b6536549d41b5aec57e0deca1f269b4f17532f0636836f587e581603a/pyogrio-0.12.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:7a0d5ca39184030aec4cde30f4258f75b227a854530d2659babc8189d76e657d", size = 23661857, upload-time = "2025-11-28T19:03:27.744Z" }, + { url = "https://files.pythonhosted.org/packages/14/78/313259e40da728bdb60106ffdc7ea8224d164498cb838ecb79b634aab967/pyogrio-0.12.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:feaff42bbe8087ca0b30e33b09d1ce049ca55fe83ad83db1139ef37d1d04f30c", size = 25237106, upload-time = "2025-11-28T19:03:30.018Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ca/5368571a8b00b941ccfbe6ea29a5566aaffd45d4eb1553b956f7755af43e/pyogrio-0.12.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:81096a5139532de5a8003ef02b41d5d2444cb382a9aecd1165b447eb549180d3", size = 31417048, upload-time = "2025-11-28T19:03:32.572Z" }, + { url = "https://files.pythonhosted.org/packages/ef/85/6eeb875f27bf498d657eb5dab9f58e4c48b36c9037122787abee9a1ba4ba/pyogrio-0.12.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:41b78863f782f7a113ed0d36a5dc74d59735bd3a82af53510899bb02a18b06bb", size = 30952115, upload-time = "2025-11-28T19:03:35.332Z" }, + { url = "https://files.pythonhosted.org/packages/36/f7/cf8bec9024625947e1a71441906f60a5fa6f9e4c441c4428037e73b1fcc8/pyogrio-0.12.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:8b65be8c4258b27cc8f919b21929cecdadda4c353e3637fa30850339ef4d15c5", size = 32537246, upload-time = "2025-11-28T19:03:37.969Z" }, + { url = "https://files.pythonhosted.org/packages/ab/10/7c9f5e428273574e69f217eba3a6c0c42936188ad4dcd9e2c41ebb711188/pyogrio-0.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:1291b866c2c81d991bda15021b08b3621709b40ee3a85689229929e9465788bf", size = 22933980, upload-time = "2025-11-28T19:03:41.047Z" }, + { url = "https://files.pythonhosted.org/packages/be/56/f56e79f71b84aa9bea25fdde39fab3846841bd7926be96f623eb7253b7e1/pyogrio-0.12.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:ec0e47a5a704e575092b2fd5c83fa0472a1d421e590f94093eb837bb0a11125d", size = 23658483, upload-time = "2025-11-28T19:03:43.567Z" }, + { url = "https://files.pythonhosted.org/packages/66/ac/5559f8a35d58a16cbb2dd7602dd11936ff8796d8c9bf789f14da88764ec3/pyogrio-0.12.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:b4c888fc08f388be4dd99dfca5e84a5cdc5994deeec0230cc45144d3460e2b21", size = 25232737, upload-time = "2025-11-28T19:03:45.92Z" }, + { url = "https://files.pythonhosted.org/packages/59/58/925f1c129ddd7cbba8dea4e7609797cea7a76dbc863ac9afd318a679c4b9/pyogrio-0.12.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:73a88436f9962750d782853727897ac2722cac5900d920e39fab3e56d7a6a7f1", size = 31377986, upload-time = "2025-11-28T19:03:48.495Z" }, + { url = "https://files.pythonhosted.org/packages/18/5f/c87034e92847b1844d0e8492a6a8e3301147d32c5e57909397ce64dbedf5/pyogrio-0.12.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:b5d248a0d59fe9bbf9a35690b70004c67830ee0ebe7d4f7bb8ffd8659f684b3a", size = 30915791, upload-time = "2025-11-28T19:03:51.267Z" }, + { url = "https://files.pythonhosted.org/packages/46/35/b874f79d03e9f900012cf609f7fff97b77164f2e14ee5aac282f8a999c1b/pyogrio-0.12.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:0622bc1a186421547660271083079b38d42e6f868802936d8538c0b379f1ab6b", size = 32499754, upload-time = "2025-11-28T19:03:58.776Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c4/705678c9c4200130290b3a104b45c0cc10aaa48fcef3b2585b34e34ab3e1/pyogrio-0.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:207bd60c7ffbcea84584596e3637653aa7095e9ee20fa408f90c7f9460392613", size = 22933945, upload-time = "2025-11-28T19:04:01.551Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e0/d92d4944001330bc87742d43f112d63d12fc89378b6187e62ff3fc1e8e85/pyogrio-0.12.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:1511b39a283fa27cda906cd187a791578942a87a40b6a06697d9b43bb8ac80b0", size = 23692697, upload-time = "2025-11-28T19:04:04.208Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d7/40acbe06d1b1140e3bb27b79e9163776469c1dc785f1be7d9a7fc7b95c87/pyogrio-0.12.1-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:e486cd6aa9ea8a15394a5f84e019d61ec18f257eeeb642348bd68c3d1e57280b", size = 25258083, upload-time = "2025-11-28T19:04:07.121Z" }, + { url = "https://files.pythonhosted.org/packages/87/a1/39fefd9cddd95986700524f43d3093b4350f6e4fc200623c3838424a5080/pyogrio-0.12.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d3f1a19f63bfd1d3042e45f37ad1d6598123a5a604b6c4ba3f38b419273486cd", size = 31368995, upload-time = "2025-11-28T19:04:09.88Z" }, + { url = "https://files.pythonhosted.org/packages/18/d7/da88c566e67d741a03851eb8d01358949d52e0b0fc2cd953582dc6d89ff8/pyogrio-0.12.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:f3dcc59b3316b8a0f59346bcc638a4d69997864a4d21da839192f50c4c92369a", size = 31035589, upload-time = "2025-11-28T19:04:12.993Z" }, + { url = "https://files.pythonhosted.org/packages/11/ac/8f0199f0d31b8ddbc4b4ea1918df8070fdf3e0a63100b898633ec9396224/pyogrio-0.12.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:a0643e041dee3e8e038fce69f52a915ecb486e6d7b674c0f9919f3c9e9629689", size = 32487973, upload-time = "2025-11-28T19:04:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/bd/64/8541a27e9635a335835d234dfaeb19d6c26097fd88224eda7791f83ca98d/pyogrio-0.12.1-cp313-cp313t-win_amd64.whl", hash = "sha256:5881017f29e110d3613819667657844d8e961b747f2d35cf92f273c27af6d068", size = 22987374, upload-time = "2025-11-28T19:04:18.91Z" }, + { url = "https://files.pythonhosted.org/packages/f4/6f/b4d5e285e08c0c60bcc23b50d73038ddc7335d8de79cc25678cd486a3db0/pyogrio-0.12.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:5a1b0453d1c9e7b03715dd57296c8f3790acb8b50d7e3b5844b3074a18f50709", size = 23660673, upload-time = "2025-11-28T19:04:21.662Z" }, + { url = "https://files.pythonhosted.org/packages/8d/75/4b29e71489c5551aa1a1c5ca8c5160a60203c94f2f68c87c0e3614d58965/pyogrio-0.12.1-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e7ee560422239dd09ca7f8284cc8483a8919c30d25f3049bb0249bff4c38dec4", size = 25232194, upload-time = "2025-11-28T19:04:23.975Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/e9929d2261a07c36301983de2767bcde90d441ab5bf1d767ce56dd07f8b4/pyogrio-0.12.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:648c6f7f5f214d30e6cf493b4af1d59782907ac068af9119ca35f18153d6865a", size = 31336936, upload-time = "2025-11-28T19:04:26.594Z" }, + { url = "https://files.pythonhosted.org/packages/1d/9e/c59941d734ed936d4e5c89b4b99cb5541307cc42b3fd466ee78a1850c177/pyogrio-0.12.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:58042584f3fd4cabb0f55d26c1405053f656be8a5c266c38140316a1e981aca0", size = 30902210, upload-time = "2025-11-28T19:04:29.143Z" }, + { url = "https://files.pythonhosted.org/packages/d1/68/cc07320a63f9c2586e60bf11d148b00e12d0e707673bffe609bbdcb7e754/pyogrio-0.12.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:b438e38e4ccbaedaa5cb5824ff5de5539315d9b2fde6547c1e816576924ee8ca", size = 32461674, upload-time = "2025-11-28T19:04:31.792Z" }, + { url = "https://files.pythonhosted.org/packages/13/bc/e4522f429c45a3b6ad28185849dd76e5c8718b780883c4795e7ee41841ae/pyogrio-0.12.1-cp314-cp314-win_amd64.whl", hash = "sha256:f1d8d8a2fea3781dc2a05982c050259261ebc0f6c5e03732d6d79d582adf9363", size = 23550575, upload-time = "2025-11-28T19:04:34.556Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ac/34f0664d0e391994a7b68529ae07a96432b2b4926dbac173ddc4ec94d310/pyogrio-0.12.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:9fe7286946f35a73e6370dc5855bc7a5e8e7babf9e4a8bad7a3279a1d94c7ea9", size = 23694285, upload-time = "2025-11-28T19:04:37.833Z" }, + { url = "https://files.pythonhosted.org/packages/8a/93/873255529faff1da09d0b27287e85ec805a318c60c0c74fd7df77f94e557/pyogrio-0.12.1-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:2c50345b382f1be801d654ec22c70ee974d6057d4ba7afe984b55f2192bc94ee", size = 25259825, upload-time = "2025-11-28T19:04:40.125Z" }, + { url = "https://files.pythonhosted.org/packages/27/95/4d4c3644695d99c6fa0b0b42f0d6266ae9dfaf64478a3371eaac950bdd02/pyogrio-0.12.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0db95765ac0ca935c7fe579e29451294e3ab19c317b0c59c31fbe92a69155e0", size = 31371995, upload-time = "2025-11-28T19:04:42.736Z" }, + { url = "https://files.pythonhosted.org/packages/4c/6f/71f6bcca8754c8bf55a4b7153c61c91f8ac5ba992568e9fa3e54a0ee76fd/pyogrio-0.12.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:fc882779075982b93064b3bf3d8642514a6df00d9dd752493b104817072cfb01", size = 31035498, upload-time = "2025-11-28T19:04:45.79Z" }, + { url = "https://files.pythonhosted.org/packages/fd/47/75c1aa165a988347317afab9b938a01ad25dbca559b582ea34473703dc38/pyogrio-0.12.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:806f620e0c54b54dbdd65e9b6368d24f344cda84c9343364b40a57eb3e1c4dca", size = 32496390, upload-time = "2025-11-28T19:04:48.786Z" }, + { url = "https://files.pythonhosted.org/packages/31/93/4641dc5d952f6bdb71dabad2c50e3f8a5d58396cdea6ff8f8a08bfd4f4a6/pyogrio-0.12.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5399f66730978d8852ef5f44dbafa0f738e7f28f4f784349f36830b69a9d2134", size = 23620996, upload-time = "2025-11-28T19:04:51.132Z" }, +] + +[[package]] +name = "pyproj" +version = "3.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/90/67bd7260b4ea9b8b20b4f58afef6c223ecb3abf368eb4ec5bc2cdef81b49/pyproj-3.7.2.tar.gz", hash = "sha256:39a0cf1ecc7e282d1d30f36594ebd55c9fae1fda8a2622cee5d100430628f88c", size = 226279, upload-time = "2025-08-14T12:05:42.18Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/ab/9893ea9fb066be70ed9074ae543914a618c131ed8dff2da1e08b3a4df4db/pyproj-3.7.2-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:0a9bb26a6356fb5b033433a6d1b4542158fb71e3c51de49b4c318a1dff3aeaab", size = 6219832, upload-time = "2025-08-14T12:04:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/53/78/4c64199146eed7184eb0e85bedec60a4aa8853b6ffe1ab1f3a8b962e70a0/pyproj-3.7.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:567caa03021178861fad27fabde87500ec6d2ee173dd32f3e2d9871e40eebd68", size = 4620650, upload-time = "2025-08-14T12:04:11.978Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ac/14a78d17943898a93ef4f8c6a9d4169911c994e3161e54a7cedeba9d8dde/pyproj-3.7.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c203101d1dc3c038a56cff0447acc515dd29d6e14811406ac539c21eed422b2a", size = 9667087, upload-time = "2025-08-14T12:04:13.964Z" }, + { url = "https://files.pythonhosted.org/packages/b8/be/212882c450bba74fc8d7d35cbd57e4af84792f0a56194819d98106b075af/pyproj-3.7.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:1edc34266c0c23ced85f95a1ee8b47c9035eae6aca5b6b340327250e8e281630", size = 9552797, upload-time = "2025-08-14T12:04:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c0/c0f25c87b5d2a8686341c53c1792a222a480d6c9caf60311fec12c99ec26/pyproj-3.7.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aa9f26c21bc0e2dc3d224cb1eb4020cf23e76af179a7c66fea49b828611e4260", size = 10837036, upload-time = "2025-08-14T12:04:18.733Z" }, + { url = "https://files.pythonhosted.org/packages/5d/37/5cbd6772addde2090c91113332623a86e8c7d583eccb2ad02ea634c4a89f/pyproj-3.7.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9428b318530625cb389b9ddc9c51251e172808a4af79b82809376daaeabe5e9", size = 10775952, upload-time = "2025-08-14T12:04:20.709Z" }, + { url = "https://files.pythonhosted.org/packages/69/a1/dc250e3cf83eb4b3b9a2cf86fdb5e25288bd40037ae449695550f9e96b2f/pyproj-3.7.2-cp312-cp312-win32.whl", hash = "sha256:b3d99ed57d319da042f175f4554fc7038aa4bcecc4ac89e217e350346b742c9d", size = 5898872, upload-time = "2025-08-14T12:04:22.485Z" }, + { url = "https://files.pythonhosted.org/packages/4a/a6/6fe724b72b70f2b00152d77282e14964d60ab092ec225e67c196c9b463e5/pyproj-3.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:11614a054cd86a2ed968a657d00987a86eeb91fdcbd9ad3310478685dc14a128", size = 6312176, upload-time = "2025-08-14T12:04:24.736Z" }, + { url = "https://files.pythonhosted.org/packages/5d/68/915cc32c02a91e76d02c8f55d5a138d6ef9e47a0d96d259df98f4842e558/pyproj-3.7.2-cp312-cp312-win_arm64.whl", hash = "sha256:509a146d1398bafe4f53273398c3bb0b4732535065fa995270e52a9d3676bca3", size = 6233452, upload-time = "2025-08-14T12:04:27.287Z" }, + { url = "https://files.pythonhosted.org/packages/be/14/faf1b90d267cea68d7e70662e7f88cefdb1bc890bd596c74b959e0517a72/pyproj-3.7.2-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:19466e529b1b15eeefdf8ff26b06fa745856c044f2f77bf0edbae94078c1dfa1", size = 6214580, upload-time = "2025-08-14T12:04:28.804Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/da9a45b184d375f62667f62eba0ca68569b0bd980a0bb7ffcc1d50440520/pyproj-3.7.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:c79b9b84c4a626c5dc324c0d666be0bfcebd99f7538d66e8898c2444221b3da7", size = 4615388, upload-time = "2025-08-14T12:04:30.553Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e7/d2b459a4a64bca328b712c1b544e109df88e5c800f7c143cfbc404d39bfb/pyproj-3.7.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ceecf374cacca317bc09e165db38ac548ee3cad07c3609442bd70311c59c21aa", size = 9628455, upload-time = "2025-08-14T12:04:32.435Z" }, + { url = "https://files.pythonhosted.org/packages/f8/85/c2b1706e51942de19076eff082f8495e57d5151364e78b5bef4af4a1d94a/pyproj-3.7.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5141a538ffdbe4bfd157421828bb2e07123a90a7a2d6f30fa1462abcfb5ce681", size = 9514269, upload-time = "2025-08-14T12:04:34.599Z" }, + { url = "https://files.pythonhosted.org/packages/34/38/07a9b89ae7467872f9a476883a5bad9e4f4d1219d31060f0f2b282276cbe/pyproj-3.7.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f000841e98ea99acbb7b8ca168d67773b0191de95187228a16110245c5d954d5", size = 10808437, upload-time = "2025-08-14T12:04:36.485Z" }, + { url = "https://files.pythonhosted.org/packages/12/56/fda1daeabbd39dec5b07f67233d09f31facb762587b498e6fc4572be9837/pyproj-3.7.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8115faf2597f281a42ab608ceac346b4eb1383d3b45ab474fd37341c4bf82a67", size = 10745540, upload-time = "2025-08-14T12:04:38.568Z" }, + { url = "https://files.pythonhosted.org/packages/0d/90/c793182cbba65a39a11db2ac6b479fe76c59e6509ae75e5744c344a0da9d/pyproj-3.7.2-cp313-cp313-win32.whl", hash = "sha256:f18c0579dd6be00b970cb1a6719197fceecc407515bab37da0066f0184aafdf3", size = 5896506, upload-time = "2025-08-14T12:04:41.059Z" }, + { url = "https://files.pythonhosted.org/packages/be/0f/747974129cf0d800906f81cd25efd098c96509026e454d4b66868779ab04/pyproj-3.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:bb41c29d5f60854b1075853fe80c58950b398d4ebb404eb532536ac8d2834ed7", size = 6310195, upload-time = "2025-08-14T12:04:42.974Z" }, + { url = "https://files.pythonhosted.org/packages/82/64/fc7598a53172c4931ec6edf5228280663063150625d3f6423b4c20f9daff/pyproj-3.7.2-cp313-cp313-win_arm64.whl", hash = "sha256:2b617d573be4118c11cd96b8891a0b7f65778fa7733ed8ecdb297a447d439100", size = 6230748, upload-time = "2025-08-14T12:04:44.491Z" }, + { url = "https://files.pythonhosted.org/packages/aa/f0/611dd5cddb0d277f94b7af12981f56e1441bf8d22695065d4f0df5218498/pyproj-3.7.2-cp313-cp313t-macosx_13_0_x86_64.whl", hash = "sha256:d27b48f0e81beeaa2b4d60c516c3a1cfbb0c7ff6ef71256d8e9c07792f735279", size = 6241729, upload-time = "2025-08-14T12:04:46.274Z" }, + { url = "https://files.pythonhosted.org/packages/15/93/40bd4a6c523ff9965e480870611aed7eda5aa2c6128c6537345a2b77b542/pyproj-3.7.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:55a3610d75023c7b1c6e583e48ef8f62918e85a2ae81300569d9f104d6684bb6", size = 4652497, upload-time = "2025-08-14T12:04:48.203Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ae/7150ead53c117880b35e0d37960d3138fe640a235feb9605cb9386f50bb0/pyproj-3.7.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:8d7349182fa622696787cc9e195508d2a41a64765da9b8a6bee846702b9e6220", size = 9942610, upload-time = "2025-08-14T12:04:49.652Z" }, + { url = "https://files.pythonhosted.org/packages/d8/17/7a4a7eafecf2b46ab64e5c08176c20ceb5844b503eaa551bf12ccac77322/pyproj-3.7.2-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:d230b186eb876ed4f29a7c5ee310144c3a0e44e89e55f65fb3607e13f6db337c", size = 9692390, upload-time = "2025-08-14T12:04:51.731Z" }, + { url = "https://files.pythonhosted.org/packages/c3/55/ae18f040f6410f0ea547a21ada7ef3e26e6c82befa125b303b02759c0e9d/pyproj-3.7.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:237499c7862c578d0369e2b8ac56eec550e391a025ff70e2af8417139dabb41c", size = 11047596, upload-time = "2025-08-14T12:04:53.748Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2e/d3fff4d2909473f26ae799f9dda04caa322c417a51ff3b25763f7d03b233/pyproj-3.7.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8c225f5978abd506fd9a78eaaf794435e823c9156091cabaab5374efb29d7f69", size = 10896975, upload-time = "2025-08-14T12:04:55.875Z" }, + { url = "https://files.pythonhosted.org/packages/f2/bc/8fc7d3963d87057b7b51ebe68c1e7c51c23129eee5072ba6b86558544a46/pyproj-3.7.2-cp313-cp313t-win32.whl", hash = "sha256:2da731876d27639ff9d2d81c151f6ab90a1546455fabd93368e753047be344a2", size = 5953057, upload-time = "2025-08-14T12:04:58.466Z" }, + { url = "https://files.pythonhosted.org/packages/cc/27/ea9809966cc47d2d51e6d5ae631ea895f7c7c7b9b3c29718f900a8f7d197/pyproj-3.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:f54d91ae18dd23b6c0ab48126d446820e725419da10617d86a1b69ada6d881d3", size = 6375414, upload-time = "2025-08-14T12:04:59.861Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/1ef0129fba9a555c658e22af68989f35e7ba7b9136f25758809efec0cd6e/pyproj-3.7.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fc52ba896cfc3214dc9f9ca3c0677a623e8fdd096b257c14a31e719d21ff3fdd", size = 6262501, upload-time = "2025-08-14T12:05:01.39Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/c2b050d3f5b71b6edd0d96ae16c990fdc42a5f1366464a5c2772146de33a/pyproj-3.7.2-cp314-cp314-macosx_13_0_x86_64.whl", hash = "sha256:2aaa328605ace41db050d06bac1adc11f01b71fe95c18661497763116c3a0f02", size = 6214541, upload-time = "2025-08-14T12:05:03.166Z" }, + { url = "https://files.pythonhosted.org/packages/03/68/68ada9c8aea96ded09a66cfd9bf87aa6db8c2edebe93f5bf9b66b0143fbc/pyproj-3.7.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:35dccbce8201313c596a970fde90e33605248b66272595c061b511c8100ccc08", size = 4617456, upload-time = "2025-08-14T12:05:04.563Z" }, + { url = "https://files.pythonhosted.org/packages/81/e4/4c50ceca7d0e937977866b02cb64e6ccf4df979a5871e521f9e255df6073/pyproj-3.7.2-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:25b0b7cb0042444c29a164b993c45c1b8013d6c48baa61dc1160d834a277e83b", size = 9615590, upload-time = "2025-08-14T12:05:06.094Z" }, + { url = "https://files.pythonhosted.org/packages/05/1e/ada6fb15a1d75b5bd9b554355a69a798c55a7dcc93b8d41596265c1772e3/pyproj-3.7.2-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:85def3a6388e9ba51f964619aa002a9d2098e77c6454ff47773bb68871024281", size = 9474960, upload-time = "2025-08-14T12:05:07.973Z" }, + { url = "https://files.pythonhosted.org/packages/51/07/9d48ad0a8db36e16f842f2c8a694c1d9d7dcf9137264846bef77585a71f3/pyproj-3.7.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b1bccefec3875ab81eabf49059e2b2ea77362c178b66fd3528c3e4df242f1516", size = 10799478, upload-time = "2025-08-14T12:05:14.102Z" }, + { url = "https://files.pythonhosted.org/packages/85/cf/2f812b529079f72f51ff2d6456b7fef06c01735e5cfd62d54ffb2b548028/pyproj-3.7.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d5371ca114d6990b675247355a801925814eca53e6c4b2f1b5c0a956336ee36e", size = 10710030, upload-time = "2025-08-14T12:05:16.317Z" }, + { url = "https://files.pythonhosted.org/packages/99/9b/4626a19e1f03eba4c0e77b91a6cf0f73aa9cb5d51a22ee385c22812bcc2c/pyproj-3.7.2-cp314-cp314-win32.whl", hash = "sha256:77f066626030f41be543274f5ac79f2a511fe89860ecd0914f22131b40a0ec25", size = 5991181, upload-time = "2025-08-14T12:05:19.492Z" }, + { url = "https://files.pythonhosted.org/packages/04/b2/5a6610554306a83a563080c2cf2c57565563eadd280e15388efa00fb5b33/pyproj-3.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:5a964da1696b8522806f4276ab04ccfff8f9eb95133a92a25900697609d40112", size = 6434721, upload-time = "2025-08-14T12:05:21.022Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ce/6c910ea2e1c74ef673c5d48c482564b8a7824a44c4e35cca2e765b68cfcc/pyproj-3.7.2-cp314-cp314-win_arm64.whl", hash = "sha256:e258ab4dbd3cf627809067c0ba8f9884ea76c8e5999d039fb37a1619c6c3e1f6", size = 6363821, upload-time = "2025-08-14T12:05:22.627Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e4/5532f6f7491812ba782a2177fe9de73fd8e2912b59f46a1d056b84b9b8f2/pyproj-3.7.2-cp314-cp314t-macosx_13_0_x86_64.whl", hash = "sha256:bbbac2f930c6d266f70ec75df35ef851d96fdb3701c674f42fd23a9314573b37", size = 6241773, upload-time = "2025-08-14T12:05:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/0938c3f2bbbef1789132d1726d9b0e662f10cfc22522743937f421ad664e/pyproj-3.7.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:b7544e0a3d6339dc9151e9c8f3ea62a936ab7cc446a806ec448bbe86aebb979b", size = 4652537, upload-time = "2025-08-14T12:05:26.391Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a8/488b1ed47d25972f33874f91f09ca8f2227902f05f63a2b80dc73e7b1c97/pyproj-3.7.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:f7f5133dca4c703e8acadf6f30bc567d39a42c6af321e7f81975c2518f3ed357", size = 9940864, upload-time = "2025-08-14T12:05:27.985Z" }, + { url = "https://files.pythonhosted.org/packages/c7/cc/7f4c895d0cb98e47b6a85a6d79eaca03eb266129eed2f845125c09cf31ff/pyproj-3.7.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:5aff3343038d7426aa5076f07feb88065f50e0502d1b0d7c22ddfdd2c75a3f81", size = 9688868, upload-time = "2025-08-14T12:05:30.425Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/c7e306b8bb0f071d9825b753ee4920f066c40fbfcce9372c4f3cfb2fc4ed/pyproj-3.7.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b0552178c61f2ac1c820d087e8ba6e62b29442debddbb09d51c4bf8acc84d888", size = 11045910, upload-time = "2025-08-14T12:05:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/42/fb/538a4d2df695980e2dde5c04d965fbdd1fe8c20a3194dc4aaa3952a4d1be/pyproj-3.7.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:47d87db2d2c436c5fd0409b34d70bb6cdb875cca2ebe7a9d1c442367b0ab8d59", size = 10895724, upload-time = "2025-08-14T12:05:35.465Z" }, + { url = "https://files.pythonhosted.org/packages/e8/8b/a3f0618b03957de9db5489a04558a8826f43906628bb0b766033aa3b5548/pyproj-3.7.2-cp314-cp314t-win32.whl", hash = "sha256:c9b6f1d8ad3e80a0ee0903a778b6ece7dca1d1d40f6d114ae01bc8ddbad971aa", size = 6056848, upload-time = "2025-08-14T12:05:37.553Z" }, + { url = "https://files.pythonhosted.org/packages/bc/56/413240dd5149dd3291eda55aa55a659da4431244a2fd1319d0ae89407cfb/pyproj-3.7.2-cp314-cp314t-win_amd64.whl", hash = "sha256:1914e29e27933ba6f9822663ee0600f169014a2859f851c054c88cf5ea8a333c", size = 6517676, upload-time = "2025-08-14T12:05:39.126Z" }, + { url = "https://files.pythonhosted.org/packages/15/73/a7141a1a0559bf1a7aa42a11c879ceb19f02f5c6c371c6d57fd86cefd4d1/pyproj-3.7.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d9d25bae416a24397e0d85739f84d323b55f6511e45a522dd7d7eae70d10c7e4", size = 6391844, upload-time = "2025-08-14T12:05:40.745Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rich" +version = "14.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/84/4831f881aa6ff3c976f6d6809b58cdfa350593ffc0dc3c58f5f6586780fb/rich-14.3.1.tar.gz", hash = "sha256:b8c5f568a3a749f9290ec6bddedf835cec33696bfc1e48bcfecb276c7386e4b8", size = 230125, upload-time = "2026-01-24T21:40:44.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/2a/a1810c8627b9ec8c57ec5ec325d306701ae7be50235e8fd81266e002a3cc/rich-14.3.1-py3-none-any.whl", hash = "sha256:da750b1aebbff0b372557426fb3f35ba56de8ef954b3190315eb64076d6fb54e", size = 309952, upload-time = "2026-01-24T21:40:42.969Z" }, +] + +[[package]] +name = "s3fs" +version = "2026.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiobotocore" }, + { name = "aiohttp" }, + { name = "fsspec" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/f2/d6e725d4a037fe65fe341d3c16e7b6f3e69a198d6115c77b0c45dffaebe7/s3fs-2026.1.0.tar.gz", hash = "sha256:b7a352dfb9553a2263b7ea4575d90cd3c64eb76cfc083b99cb90b36b31e9d09d", size = 81224, upload-time = "2026-01-09T15:29:49.025Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/cf/0af92a4d3f36dd9ff675e0419e7efc48d7808641ac2b2ce2c1f09a9dc632/s3fs-2026.1.0-py3-none-any.whl", hash = "sha256:c1f4ad1fca6dd052ffaa104a293ba209772f4a60c164818382833868e1b1597d", size = 30713, upload-time = "2026-01-09T15:29:47.418Z" }, +] + +[[package]] +name = "scikit-image" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "imageio" }, + { name = "lazy-loader" }, + { name = "networkx" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "scipy" }, + { name = "tifffile" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/b4/2528bb43c67d48053a7a649a9666432dc307d66ba02e3a6d5c40f46655df/scikit_image-0.26.0.tar.gz", hash = "sha256:f5f970ab04efad85c24714321fcc91613fcb64ef2a892a13167df2f3e59199fa", size = 22729739, upload-time = "2025-12-20T17:12:21.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/e8/e13757982264b33a1621628f86b587e9a73a13f5256dad49b19ba7dc9083/scikit_image-0.26.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d454b93a6fa770ac5ae2d33570f8e7a321bb80d29511ce4b6b78058ebe176e8c", size = 12376452, upload-time = "2025-12-20T17:10:52.796Z" }, + { url = "https://files.pythonhosted.org/packages/e3/be/f8dd17d0510f9911f9f17ba301f7455328bf13dae416560126d428de9568/scikit_image-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3409e89d66eff5734cd2b672d1c48d2759360057e714e1d92a11df82c87cba37", size = 12061567, upload-time = "2025-12-20T17:10:55.207Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/c70120a6880579fb42b91567ad79feb4772f7be72e8d52fec403a3dde0c6/scikit_image-0.26.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c717490cec9e276afb0438dd165b7c3072d6c416709cc0f9f5a4c1070d23a44", size = 13084214, upload-time = "2025-12-20T17:10:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a2/70401a107d6d7466d64b466927e6b96fcefa99d57494b972608e2f8be50f/scikit_image-0.26.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7df650e79031634ac90b11e64a9eedaf5a5e06fcd09bcd03a34be01745744466", size = 13561683, upload-time = "2025-12-20T17:10:59.49Z" }, + { url = "https://files.pythonhosted.org/packages/13/a5/48bdfd92794c5002d664e0910a349d0a1504671ef5ad358150f21643c79a/scikit_image-0.26.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cefd85033e66d4ea35b525bb0937d7f42d4cdcfed2d1888e1570d5ce450d3932", size = 14112147, upload-time = "2025-12-20T17:11:02.083Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b5/ac71694da92f5def5953ca99f18a10fe98eac2dd0a34079389b70b4d0394/scikit_image-0.26.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3f5bf622d7c0435884e1e141ebbe4b2804e16b2dd23ae4c6183e2ea99233be70", size = 14661625, upload-time = "2025-12-20T17:11:04.528Z" }, + { url = "https://files.pythonhosted.org/packages/23/4d/a3cc1e96f080e253dad2251bfae7587cf2b7912bcd76fd43fd366ff35a87/scikit_image-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:abed017474593cd3056ae0fe948d07d0747b27a085e92df5474f4955dd65aec0", size = 11911059, upload-time = "2025-12-20T17:11:06.61Z" }, + { url = "https://files.pythonhosted.org/packages/35/8a/d1b8055f584acc937478abf4550d122936f420352422a1a625eef2c605d8/scikit_image-0.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:4d57e39ef67a95d26860c8caf9b14b8fb130f83b34c6656a77f191fa6d1d04d8", size = 11348740, upload-time = "2025-12-20T17:11:09.118Z" }, + { url = "https://files.pythonhosted.org/packages/4f/48/02357ffb2cca35640f33f2cfe054a4d6d5d7a229b88880a64f1e45c11f4e/scikit_image-0.26.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a2e852eccf41d2d322b8e60144e124802873a92b8d43a6f96331aa42888491c7", size = 12346329, upload-time = "2025-12-20T17:11:11.599Z" }, + { url = "https://files.pythonhosted.org/packages/67/b9/b792c577cea2c1e94cda83b135a656924fc57c428e8a6d302cd69aac1b60/scikit_image-0.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:98329aab3bc87db352b9887f64ce8cdb8e75f7c2daa19927f2e121b797b678d5", size = 12031726, upload-time = "2025-12-20T17:11:13.871Z" }, + { url = "https://files.pythonhosted.org/packages/07/a9/9564250dfd65cb20404a611016db52afc6268b2b371cd19c7538ea47580f/scikit_image-0.26.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:915bb3ba66455cf8adac00dc8fdf18a4cd29656aec7ddd38cb4dda90289a6f21", size = 13094910, upload-time = "2025-12-20T17:11:16.2Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b8/0d8eeb5a9fd7d34ba84f8a55753a0a3e2b5b51b2a5a0ade648a8db4a62f7/scikit_image-0.26.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b36ab5e778bf50af5ff386c3ac508027dc3aaeccf2161bdf96bde6848f44d21b", size = 13660939, upload-time = "2025-12-20T17:11:18.464Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d6/91d8973584d4793d4c1a847d388e34ef1218d835eeddecfc9108d735b467/scikit_image-0.26.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:09bad6a5d5949c7896c8347424c4cca899f1d11668030e5548813ab9c2865dcb", size = 14138938, upload-time = "2025-12-20T17:11:20.919Z" }, + { url = "https://files.pythonhosted.org/packages/39/9a/7e15d8dc10d6bbf212195fb39bdeb7f226c46dd53f9c63c312e111e2e175/scikit_image-0.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:aeb14db1ed09ad4bee4ceb9e635547a8d5f3549be67fc6c768c7f923e027e6cd", size = 14752243, upload-time = "2025-12-20T17:11:23.347Z" }, + { url = "https://files.pythonhosted.org/packages/8f/58/2b11b933097bc427e42b4a8b15f7de8f24f2bac1fd2779d2aea1431b2c31/scikit_image-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:ac529eb9dbd5954f9aaa2e3fe9a3fd9661bfe24e134c688587d811a0233127f1", size = 11906770, upload-time = "2025-12-20T17:11:25.297Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ec/96941474a18a04b69b6f6562a5bd79bd68049fa3728d3b350976eccb8b93/scikit_image-0.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:a2d211bc355f59725efdcae699b93b30348a19416cc9e017f7b2fb599faf7219", size = 11342506, upload-time = "2025-12-20T17:11:27.399Z" }, + { url = "https://files.pythonhosted.org/packages/03/e5/c1a9962b0cf1952f42d32b4a2e48eed520320dbc4d2ff0b981c6fa508b6b/scikit_image-0.26.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9eefb4adad066da408a7601c4c24b07af3b472d90e08c3e7483d4e9e829d8c49", size = 12663278, upload-time = "2025-12-20T17:11:29.358Z" }, + { url = "https://files.pythonhosted.org/packages/ae/97/c1a276a59ce8e4e24482d65c1a3940d69c6b3873279193b7ebd04e5ee56b/scikit_image-0.26.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6caec76e16c970c528d15d1c757363334d5cb3069f9cea93d2bead31820511f3", size = 12405142, upload-time = "2025-12-20T17:11:31.282Z" }, + { url = "https://files.pythonhosted.org/packages/d4/4a/f1cbd1357caef6c7993f7efd514d6e53d8fd6f7fe01c4714d51614c53289/scikit_image-0.26.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a07200fe09b9d99fcdab959859fe0f7db8df6333d6204344425d476850ce3604", size = 12942086, upload-time = "2025-12-20T17:11:33.683Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6f/74d9fb87c5655bd64cf00b0c44dc3d6206d9002e5f6ba1c9aeb13236f6bf/scikit_image-0.26.0-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92242351bccf391fc5df2d1529d15470019496d2498d615beb68da85fe7fdf37", size = 13265667, upload-time = "2025-12-20T17:11:36.11Z" }, + { url = "https://files.pythonhosted.org/packages/a7/73/faddc2413ae98d863f6fa2e3e14da4467dd38e788e1c23346cf1a2b06b97/scikit_image-0.26.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:52c496f75a7e45844d951557f13c08c81487c6a1da2e3c9c8a39fcde958e02cc", size = 14001966, upload-time = "2025-12-20T17:11:38.55Z" }, + { url = "https://files.pythonhosted.org/packages/02/94/9f46966fa042b5d57c8cd641045372b4e0df0047dd400e77ea9952674110/scikit_image-0.26.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:20ef4a155e2e78b8ab973998e04d8a361d49d719e65412405f4dadd9155a61d9", size = 14359526, upload-time = "2025-12-20T17:11:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b4/2840fe38f10057f40b1c9f8fb98a187a370936bf144a4ac23452c5ef1baf/scikit_image-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:c9087cf7d0e7f33ab5c46d2068d86d785e70b05400a891f73a13400f1e1faf6a", size = 12287629, upload-time = "2025-12-20T17:11:43.11Z" }, + { url = "https://files.pythonhosted.org/packages/22/ba/73b6ca70796e71f83ab222690e35a79612f0117e5aaf167151b7d46f5f2c/scikit_image-0.26.0-cp313-cp313t-win_arm64.whl", hash = "sha256:27d58bc8b2acd351f972c6508c1b557cfed80299826080a4d803dd29c51b707e", size = 11647755, upload-time = "2025-12-20T17:11:45.279Z" }, + { url = "https://files.pythonhosted.org/packages/51/44/6b744f92b37ae2833fd423cce8f806d2368859ec325a699dc30389e090b9/scikit_image-0.26.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:63af3d3a26125f796f01052052f86806da5b5e54c6abef152edb752683075a9c", size = 12365810, upload-time = "2025-12-20T17:11:47.357Z" }, + { url = "https://files.pythonhosted.org/packages/40/f5/83590d9355191f86ac663420fec741b82cc547a4afe7c4c1d986bf46e4db/scikit_image-0.26.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ce00600cd70d4562ed59f80523e18cdcc1fae0e10676498a01f73c255774aefd", size = 12075717, upload-time = "2025-12-20T17:11:49.483Z" }, + { url = "https://files.pythonhosted.org/packages/72/48/253e7cf5aee6190459fe136c614e2cbccc562deceb4af96e0863f1b8ee29/scikit_image-0.26.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6381edf972b32e4f54085449afde64365a57316637496c1325a736987083e2ab", size = 13161520, upload-time = "2025-12-20T17:11:51.58Z" }, + { url = "https://files.pythonhosted.org/packages/73/c3/cec6a3cbaadfdcc02bd6ff02f3abfe09eaa7f4d4e0a525a1e3a3f4bce49c/scikit_image-0.26.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6624a76c6085218248154cc7e1500e6b488edcd9499004dd0d35040607d7505", size = 13684340, upload-time = "2025-12-20T17:11:53.708Z" }, + { url = "https://files.pythonhosted.org/packages/d4/0d/39a776f675d24164b3a267aa0db9f677a4cb20127660d8bf4fd7fef66817/scikit_image-0.26.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f775f0e420faac9c2aa6757135f4eb468fb7b70e0b67fa77a5e79be3c30ee331", size = 14203839, upload-time = "2025-12-20T17:11:55.89Z" }, + { url = "https://files.pythonhosted.org/packages/ee/25/2514df226bbcedfe9b2caafa1ba7bc87231a0c339066981b182b08340e06/scikit_image-0.26.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede4d6d255cc5da9faeb2f9ba7fedbc990abbc652db429f40a16b22e770bb578", size = 14770021, upload-time = "2025-12-20T17:11:58.014Z" }, + { url = "https://files.pythonhosted.org/packages/8d/5b/0671dc91c0c79340c3fe202f0549c7d3681eb7640fe34ab68a5f090a7c7f/scikit_image-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:0660b83968c15293fd9135e8d860053ee19500d52bf55ca4fb09de595a1af650", size = 12023490, upload-time = "2025-12-20T17:12:00.013Z" }, + { url = "https://files.pythonhosted.org/packages/65/08/7c4cb59f91721f3de07719085212a0b3962e3e3f2d1818cbac4eeb1ea53e/scikit_image-0.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:b8d14d3181c21c11170477a42542c1addc7072a90b986675a71266ad17abc37f", size = 11473782, upload-time = "2025-12-20T17:12:01.983Z" }, + { url = "https://files.pythonhosted.org/packages/49/41/65c4258137acef3d73cb561ac55512eacd7b30bb4f4a11474cad526bc5db/scikit_image-0.26.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:cde0bbd57e6795eba83cb10f71a677f7239271121dc950bc060482834a668ad1", size = 12686060, upload-time = "2025-12-20T17:12:03.886Z" }, + { url = "https://files.pythonhosted.org/packages/e7/32/76971f8727b87f1420a962406388a50e26667c31756126444baf6668f559/scikit_image-0.26.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:163e9afb5b879562b9aeda0dd45208a35316f26cc7a3aed54fd601604e5cf46f", size = 12422628, upload-time = "2025-12-20T17:12:05.921Z" }, + { url = "https://files.pythonhosted.org/packages/37/0d/996febd39f757c40ee7b01cdb861867327e5c8e5f595a634e8201462d958/scikit_image-0.26.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724f79fd9b6cb6f4a37864fe09f81f9f5d5b9646b6868109e1b100d1a7019e59", size = 12962369, upload-time = "2025-12-20T17:12:07.912Z" }, + { url = "https://files.pythonhosted.org/packages/48/b4/612d354f946c9600e7dea012723c11d47e8d455384e530f6daaaeb9bf62c/scikit_image-0.26.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3268f13310e6857508bd87202620df996199a016a1d281b309441d227c822394", size = 13272431, upload-time = "2025-12-20T17:12:10.255Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/26c00b466e06055a086de2c6e2145fe189ccdc9a1d11ccc7de020f2591ad/scikit_image-0.26.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fac96a1f9b06cd771cbbb3cd96c5332f36d4efd839b1d8b053f79e5887acde62", size = 14016362, upload-time = "2025-12-20T17:12:12.793Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/00a90402e1775634043c2a0af8a3c76ad450866d9fa444efcc43b553ba2d/scikit_image-0.26.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c1e7bd342f43e7a97e571b3f03ba4c1293ea1a35c3f13f41efdc8a81c1dc8f2", size = 14364151, upload-time = "2025-12-20T17:12:14.909Z" }, + { url = "https://files.pythonhosted.org/packages/da/ca/918d8d306bd43beacff3b835c6d96fac0ae64c0857092f068b88db531a7c/scikit_image-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b702c3bb115e1dcf4abf5297429b5c90f2189655888cbed14921f3d26f81d3a4", size = 12413484, upload-time = "2025-12-20T17:12:17.046Z" }, + { url = "https://files.pythonhosted.org/packages/dc/cd/4da01329b5a8d47ff7ec3c99a2b02465a8017b186027590dc7425cee0b56/scikit_image-0.26.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0608aa4a9ec39e0843de10d60edb2785a30c1c47819b67866dd223ebd149acaf", size = 11769501, upload-time = "2025-12-20T17:12:19.339Z" }, +] + +[[package]] +name = "scipy" +version = "1.16.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/ca/d8ace4f98322d01abcd52d381134344bf7b431eba7ed8b42bdea5a3c2ac9/scipy-1.16.3.tar.gz", hash = "sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb", size = 30597883, upload-time = "2025-10-28T17:38:54.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/41/5bf55c3f386b1643812f3a5674edf74b26184378ef0f3e7c7a09a7e2ca7f/scipy-1.16.3-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81fc5827606858cf71446a5e98715ba0e11f0dbc83d71c7409d05486592a45d6", size = 36659043, upload-time = "2025-10-28T17:32:40.285Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0f/65582071948cfc45d43e9870bf7ca5f0e0684e165d7c9ef4e50d783073eb/scipy-1.16.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:c97176013d404c7346bf57874eaac5187d969293bf40497140b0a2b2b7482e07", size = 28898986, upload-time = "2025-10-28T17:32:45.325Z" }, + { url = "https://files.pythonhosted.org/packages/96/5e/36bf3f0ac298187d1ceadde9051177d6a4fe4d507e8f59067dc9dd39e650/scipy-1.16.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2b71d93c8a9936046866acebc915e2af2e292b883ed6e2cbe5c34beb094b82d9", size = 20889814, upload-time = "2025-10-28T17:32:49.277Z" }, + { url = "https://files.pythonhosted.org/packages/80/35/178d9d0c35394d5d5211bbff7ac4f2986c5488b59506fef9e1de13ea28d3/scipy-1.16.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3d4a07a8e785d80289dfe66b7c27d8634a773020742ec7187b85ccc4b0e7b686", size = 23565795, upload-time = "2025-10-28T17:32:53.337Z" }, + { url = "https://files.pythonhosted.org/packages/fa/46/d1146ff536d034d02f83c8afc3c4bab2eddb634624d6529a8512f3afc9da/scipy-1.16.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0553371015692a898e1aa858fed67a3576c34edefa6b7ebdb4e9dde49ce5c203", size = 33349476, upload-time = "2025-10-28T17:32:58.353Z" }, + { url = "https://files.pythonhosted.org/packages/79/2e/415119c9ab3e62249e18c2b082c07aff907a273741b3f8160414b0e9193c/scipy-1.16.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:72d1717fd3b5e6ec747327ce9bda32d5463f472c9dce9f54499e81fbd50245a1", size = 35676692, upload-time = "2025-10-28T17:33:03.88Z" }, + { url = "https://files.pythonhosted.org/packages/27/82/df26e44da78bf8d2aeaf7566082260cfa15955a5a6e96e6a29935b64132f/scipy-1.16.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fb2472e72e24d1530debe6ae078db70fb1605350c88a3d14bc401d6306dbffe", size = 36019345, upload-time = "2025-10-28T17:33:09.773Z" }, + { url = "https://files.pythonhosted.org/packages/82/31/006cbb4b648ba379a95c87262c2855cd0d09453e500937f78b30f02fa1cd/scipy-1.16.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5192722cffe15f9329a3948c4b1db789fbb1f05c97899187dcf009b283aea70", size = 38678975, upload-time = "2025-10-28T17:33:15.809Z" }, + { url = "https://files.pythonhosted.org/packages/c2/7f/acbd28c97e990b421af7d6d6cd416358c9c293fc958b8529e0bd5d2a2a19/scipy-1.16.3-cp312-cp312-win_amd64.whl", hash = "sha256:56edc65510d1331dae01ef9b658d428e33ed48b4f77b1d51caf479a0253f96dc", size = 38555926, upload-time = "2025-10-28T17:33:21.388Z" }, + { url = "https://files.pythonhosted.org/packages/ce/69/c5c7807fd007dad4f48e0a5f2153038dc96e8725d3345b9ee31b2b7bed46/scipy-1.16.3-cp312-cp312-win_arm64.whl", hash = "sha256:a8a26c78ef223d3e30920ef759e25625a0ecdd0d60e5a8818b7513c3e5384cf2", size = 25463014, upload-time = "2025-10-28T17:33:25.975Z" }, + { url = "https://files.pythonhosted.org/packages/72/f1/57e8327ab1508272029e27eeef34f2302ffc156b69e7e233e906c2a5c379/scipy-1.16.3-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:d2ec56337675e61b312179a1ad124f5f570c00f920cc75e1000025451b88241c", size = 36617856, upload-time = "2025-10-28T17:33:31.375Z" }, + { url = "https://files.pythonhosted.org/packages/44/13/7e63cfba8a7452eb756306aa2fd9b37a29a323b672b964b4fdeded9a3f21/scipy-1.16.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:16b8bc35a4cc24db80a0ec836a9286d0e31b2503cb2fd7ff7fb0e0374a97081d", size = 28874306, upload-time = "2025-10-28T17:33:36.516Z" }, + { url = "https://files.pythonhosted.org/packages/15/65/3a9400efd0228a176e6ec3454b1fa998fbbb5a8defa1672c3f65706987db/scipy-1.16.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:5803c5fadd29de0cf27fa08ccbfe7a9e5d741bf63e4ab1085437266f12460ff9", size = 20865371, upload-time = "2025-10-28T17:33:42.094Z" }, + { url = "https://files.pythonhosted.org/packages/33/d7/eda09adf009a9fb81827194d4dd02d2e4bc752cef16737cc4ef065234031/scipy-1.16.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:b81c27fc41954319a943d43b20e07c40bdcd3ff7cf013f4fb86286faefe546c4", size = 23524877, upload-time = "2025-10-28T17:33:48.483Z" }, + { url = "https://files.pythonhosted.org/packages/7d/6b/3f911e1ebc364cb81320223a3422aab7d26c9c7973109a9cd0f27c64c6c0/scipy-1.16.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0c3b4dd3d9b08dbce0f3440032c52e9e2ab9f96ade2d3943313dfe51a7056959", size = 33342103, upload-time = "2025-10-28T17:33:56.495Z" }, + { url = "https://files.pythonhosted.org/packages/21/f6/4bfb5695d8941e5c570a04d9fcd0d36bce7511b7d78e6e75c8f9791f82d0/scipy-1.16.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7dc1360c06535ea6116a2220f760ae572db9f661aba2d88074fe30ec2aa1ff88", size = 35697297, upload-time = "2025-10-28T17:34:04.722Z" }, + { url = "https://files.pythonhosted.org/packages/04/e1/6496dadbc80d8d896ff72511ecfe2316b50313bfc3ebf07a3f580f08bd8c/scipy-1.16.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:663b8d66a8748051c3ee9c96465fb417509315b99c71550fda2591d7dd634234", size = 36021756, upload-time = "2025-10-28T17:34:13.482Z" }, + { url = "https://files.pythonhosted.org/packages/fe/bd/a8c7799e0136b987bda3e1b23d155bcb31aec68a4a472554df5f0937eef7/scipy-1.16.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eab43fae33a0c39006a88096cd7b4f4ef545ea0447d250d5ac18202d40b6611d", size = 38696566, upload-time = "2025-10-28T17:34:22.384Z" }, + { url = "https://files.pythonhosted.org/packages/cd/01/1204382461fcbfeb05b6161b594f4007e78b6eba9b375382f79153172b4d/scipy-1.16.3-cp313-cp313-win_amd64.whl", hash = "sha256:062246acacbe9f8210de8e751b16fc37458213f124bef161a5a02c7a39284304", size = 38529877, upload-time = "2025-10-28T17:35:51.076Z" }, + { url = "https://files.pythonhosted.org/packages/7f/14/9d9fbcaa1260a94f4bb5b64ba9213ceb5d03cd88841fe9fd1ffd47a45b73/scipy-1.16.3-cp313-cp313-win_arm64.whl", hash = "sha256:50a3dbf286dbc7d84f176f9a1574c705f277cb6565069f88f60db9eafdbe3ee2", size = 25455366, upload-time = "2025-10-28T17:35:59.014Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a3/9ec205bd49f42d45d77f1730dbad9ccf146244c1647605cf834b3a8c4f36/scipy-1.16.3-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:fb4b29f4cf8cc5a8d628bc8d8e26d12d7278cd1f219f22698a378c3d67db5e4b", size = 37027931, upload-time = "2025-10-28T17:34:31.451Z" }, + { url = "https://files.pythonhosted.org/packages/25/06/ca9fd1f3a4589cbd825b1447e5db3a8ebb969c1eaf22c8579bd286f51b6d/scipy-1.16.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:8d09d72dc92742988b0e7750bddb8060b0c7079606c0d24a8cc8e9c9c11f9079", size = 29400081, upload-time = "2025-10-28T17:34:39.087Z" }, + { url = "https://files.pythonhosted.org/packages/6a/56/933e68210d92657d93fb0e381683bc0e53a965048d7358ff5fbf9e6a1b17/scipy-1.16.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:03192a35e661470197556de24e7cb1330d84b35b94ead65c46ad6f16f6b28f2a", size = 21391244, upload-time = "2025-10-28T17:34:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/a8/7e/779845db03dc1418e215726329674b40576879b91814568757ff0014ad65/scipy-1.16.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:57d01cb6f85e34f0946b33caa66e892aae072b64b034183f3d87c4025802a119", size = 23929753, upload-time = "2025-10-28T17:34:51.793Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4b/f756cf8161d5365dcdef9e5f460ab226c068211030a175d2fc7f3f41ca64/scipy-1.16.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:96491a6a54e995f00a28a3c3badfff58fd093bf26cd5fb34a2188c8c756a3a2c", size = 33496912, upload-time = "2025-10-28T17:34:59.8Z" }, + { url = "https://files.pythonhosted.org/packages/09/b5/222b1e49a58668f23839ca1542a6322bb095ab8d6590d4f71723869a6c2c/scipy-1.16.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cd13e354df9938598af2be05822c323e97132d5e6306b83a3b4ee6724c6e522e", size = 35802371, upload-time = "2025-10-28T17:35:08.173Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8d/5964ef68bb31829bde27611f8c9deeac13764589fe74a75390242b64ca44/scipy-1.16.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63d3cdacb8a824a295191a723ee5e4ea7768ca5ca5f2838532d9f2e2b3ce2135", size = 36190477, upload-time = "2025-10-28T17:35:16.7Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f2/b31d75cb9b5fa4dd39a0a931ee9b33e7f6f36f23be5ef560bf72e0f92f32/scipy-1.16.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e7efa2681ea410b10dde31a52b18b0154d66f2485328830e45fdf183af5aefc6", size = 38796678, upload-time = "2025-10-28T17:35:26.354Z" }, + { url = "https://files.pythonhosted.org/packages/b4/1e/b3723d8ff64ab548c38d87055483714fefe6ee20e0189b62352b5e015bb1/scipy-1.16.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2d1ae2cf0c350e7705168ff2429962a89ad90c2d49d1dd300686d8b2a5af22fc", size = 38640178, upload-time = "2025-10-28T17:35:35.304Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f3/d854ff38789aca9b0cc23008d607ced9de4f7ab14fa1ca4329f86b3758ca/scipy-1.16.3-cp313-cp313t-win_arm64.whl", hash = "sha256:0c623a54f7b79dd88ef56da19bc2873afec9673a48f3b85b18e4d402bdd29a5a", size = 25803246, upload-time = "2025-10-28T17:35:42.155Z" }, + { url = "https://files.pythonhosted.org/packages/99/f6/99b10fd70f2d864c1e29a28bbcaa0c6340f9d8518396542d9ea3b4aaae15/scipy-1.16.3-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:875555ce62743e1d54f06cdf22c1e0bc47b91130ac40fe5d783b6dfa114beeb6", size = 36606469, upload-time = "2025-10-28T17:36:08.741Z" }, + { url = "https://files.pythonhosted.org/packages/4d/74/043b54f2319f48ea940dd025779fa28ee360e6b95acb7cd188fad4391c6b/scipy-1.16.3-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:bb61878c18a470021fb515a843dc7a76961a8daceaaaa8bad1332f1bf4b54657", size = 28872043, upload-time = "2025-10-28T17:36:16.599Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e1/24b7e50cc1c4ee6ffbcb1f27fe9f4c8b40e7911675f6d2d20955f41c6348/scipy-1.16.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2622206f5559784fa5c4b53a950c3c7c1cf3e84ca1b9c4b6c03f062f289ca26", size = 20862952, upload-time = "2025-10-28T17:36:22.966Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3a/3e8c01a4d742b730df368e063787c6808597ccb38636ed821d10b39ca51b/scipy-1.16.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7f68154688c515cdb541a31ef8eb66d8cd1050605be9dcd74199cbd22ac739bc", size = 23508512, upload-time = "2025-10-28T17:36:29.731Z" }, + { url = "https://files.pythonhosted.org/packages/1f/60/c45a12b98ad591536bfe5330cb3cfe1850d7570259303563b1721564d458/scipy-1.16.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3c820ddb80029fe9f43d61b81d8b488d3ef8ca010d15122b152db77dc94c22", size = 33413639, upload-time = "2025-10-28T17:36:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/71/bc/35957d88645476307e4839712642896689df442f3e53b0fa016ecf8a3357/scipy-1.16.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d3837938ae715fc0fe3c39c0202de3a8853aff22ca66781ddc2ade7554b7e2cc", size = 35704729, upload-time = "2025-10-28T17:36:46.547Z" }, + { url = "https://files.pythonhosted.org/packages/3b/15/89105e659041b1ca11c386e9995aefacd513a78493656e57789f9d9eab61/scipy-1.16.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aadd23f98f9cb069b3bd64ddc900c4d277778242e961751f77a8cb5c4b946fb0", size = 36086251, upload-time = "2025-10-28T17:36:55.161Z" }, + { url = "https://files.pythonhosted.org/packages/1a/87/c0ea673ac9c6cc50b3da2196d860273bc7389aa69b64efa8493bdd25b093/scipy-1.16.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b7c5f1bda1354d6a19bc6af73a649f8285ca63ac6b52e64e658a5a11d4d69800", size = 38716681, upload-time = "2025-10-28T17:37:04.1Z" }, + { url = "https://files.pythonhosted.org/packages/91/06/837893227b043fb9b0d13e4bd7586982d8136cb249ffb3492930dab905b8/scipy-1.16.3-cp314-cp314-win_amd64.whl", hash = "sha256:e5d42a9472e7579e473879a1990327830493a7047506d58d73fc429b84c1d49d", size = 39358423, upload-time = "2025-10-28T17:38:20.005Z" }, + { url = "https://files.pythonhosted.org/packages/95/03/28bce0355e4d34a7c034727505a02d19548549e190bedd13a721e35380b7/scipy-1.16.3-cp314-cp314-win_arm64.whl", hash = "sha256:6020470b9d00245926f2d5bb93b119ca0340f0d564eb6fbaad843eaebf9d690f", size = 26135027, upload-time = "2025-10-28T17:38:24.966Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6f/69f1e2b682efe9de8fe9f91040f0cd32f13cfccba690512ba4c582b0bc29/scipy-1.16.3-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:e1d27cbcb4602680a49d787d90664fa4974063ac9d4134813332a8c53dbe667c", size = 37028379, upload-time = "2025-10-28T17:37:14.061Z" }, + { url = "https://files.pythonhosted.org/packages/7c/2d/e826f31624a5ebbab1cd93d30fd74349914753076ed0593e1d56a98c4fb4/scipy-1.16.3-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:9b9c9c07b6d56a35777a1b4cc8966118fb16cfd8daf6743867d17d36cfad2d40", size = 29400052, upload-time = "2025-10-28T17:37:21.709Z" }, + { url = "https://files.pythonhosted.org/packages/69/27/d24feb80155f41fd1f156bf144e7e049b4e2b9dd06261a242905e3bc7a03/scipy-1.16.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:3a4c460301fb2cffb7f88528f30b3127742cff583603aa7dc964a52c463b385d", size = 21391183, upload-time = "2025-10-28T17:37:29.559Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d3/1b229e433074c5738a24277eca520a2319aac7465eea7310ea6ae0e98ae2/scipy-1.16.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:f667a4542cc8917af1db06366d3f78a5c8e83badd56409f94d1eac8d8d9133fa", size = 23930174, upload-time = "2025-10-28T17:37:36.306Z" }, + { url = "https://files.pythonhosted.org/packages/16/9d/d9e148b0ec680c0f042581a2be79a28a7ab66c0c4946697f9e7553ead337/scipy-1.16.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f379b54b77a597aa7ee5e697df0d66903e41b9c85a6dd7946159e356319158e8", size = 33497852, upload-time = "2025-10-28T17:37:42.228Z" }, + { url = "https://files.pythonhosted.org/packages/2f/22/4e5f7561e4f98b7bea63cf3fd7934bff1e3182e9f1626b089a679914d5c8/scipy-1.16.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4aff59800a3b7f786b70bfd6ab551001cb553244988d7d6b8299cb1ea653b353", size = 35798595, upload-time = "2025-10-28T17:37:48.102Z" }, + { url = "https://files.pythonhosted.org/packages/83/42/6644d714c179429fc7196857866f219fef25238319b650bb32dde7bf7a48/scipy-1.16.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:da7763f55885045036fabcebd80144b757d3db06ab0861415d1c3b7c69042146", size = 36186269, upload-time = "2025-10-28T17:37:53.72Z" }, + { url = "https://files.pythonhosted.org/packages/ac/70/64b4d7ca92f9cf2e6fc6aaa2eecf80bb9b6b985043a9583f32f8177ea122/scipy-1.16.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ffa6eea95283b2b8079b821dc11f50a17d0571c92b43e2b5b12764dc5f9b285d", size = 38802779, upload-time = "2025-10-28T17:37:59.393Z" }, + { url = "https://files.pythonhosted.org/packages/61/82/8d0e39f62764cce5ffd5284131e109f07cf8955aef9ab8ed4e3aa5e30539/scipy-1.16.3-cp314-cp314t-win_amd64.whl", hash = "sha256:d9f48cafc7ce94cf9b15c6bffdc443a81a27bf7075cf2dcd5c8b40f85d10c4e7", size = 39471128, upload-time = "2025-10-28T17:38:05.259Z" }, + { url = "https://files.pythonhosted.org/packages/64/47/a494741db7280eae6dc033510c319e34d42dd41b7ac0c7ead39354d1a2b5/scipy-1.16.3-cp314-cp314t-win_arm64.whl", hash = "sha256:21d9d6b197227a12dcbf9633320a4e34c6b0e51c57268df255a0942983bac562", size = 26464127, upload-time = "2025-10-28T17:38:11.34Z" }, +] + +[[package]] +name = "session-info2" +version = "0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/10/69ce3918322efdef80decd989f983532306ad10e5c66c42c70dfdd564ab8/session_info2-0.3.tar.gz", hash = "sha256:70500cf970d4eca3bc9e1e14a849a31a5bb626637d8e801d95ca51296c876100", size = 24727, upload-time = "2025-12-22T14:39:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/6a/04e51cb67eba95e0fc74b6d370ddf76cde8bcc4191398d43d30a339a9e44/session_info2-0.3-py3-none-any.whl", hash = "sha256:95cbacef200cd7ff84e6010fdc957007c8692bd9517dc57e91ad2c8831e87d61", size = 17599, upload-time = "2025-12-22T14:39:19.896Z" }, +] + +[[package]] +name = "setuptools" +version = "80.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/95/faf61eb8363f26aa7e1d762267a8d602a1b26d4f3a1e758e92cb3cb8b054/setuptools-80.10.2.tar.gz", hash = "sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70", size = 1200343, upload-time = "2026-01-25T22:38:17.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/b8/f1f62a5e3c0ad2ff1d189590bfa4c46b4f3b6e49cef6f26c6ee4e575394d/setuptools-80.10.2-py3-none-any.whl", hash = "sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173", size = 1064234, upload-time = "2026-01-25T22:38:15.216Z" }, +] + +[[package]] +name = "shapely" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489, upload-time = "2025-09-24T13:51:41.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/c0/f3b6453cf2dfa99adc0ba6675f9aaff9e526d2224cbd7ff9c1a879238693/shapely-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe2533caae6a91a543dec62e8360fe86ffcdc42a7c55f9dfd0128a977a896b94", size = 1833550, upload-time = "2025-09-24T13:50:30.019Z" }, + { url = "https://files.pythonhosted.org/packages/86/07/59dee0bc4b913b7ab59ab1086225baca5b8f19865e6101db9ebb7243e132/shapely-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba4d1333cc0bc94381d6d4308d2e4e008e0bd128bdcff5573199742ee3634359", size = 1643556, upload-time = "2025-09-24T13:50:32.291Z" }, + { url = "https://files.pythonhosted.org/packages/26/29/a5397e75b435b9895cd53e165083faed5d12fd9626eadec15a83a2411f0f/shapely-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bd308103340030feef6c111d3eb98d50dc13feea33affc8a6f9fa549e9458a3", size = 2988308, upload-time = "2025-09-24T13:50:33.862Z" }, + { url = "https://files.pythonhosted.org/packages/b9/37/e781683abac55dde9771e086b790e554811a71ed0b2b8a1e789b7430dd44/shapely-2.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e7d4d7ad262a48bb44277ca12c7c78cb1b0f56b32c10734ec9a1d30c0b0c54b", size = 3099844, upload-time = "2025-09-24T13:50:35.459Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f3/9876b64d4a5a321b9dc482c92bb6f061f2fa42131cba643c699f39317cb9/shapely-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9eddfe513096a71896441a7c37db72da0687b34752c4e193577a145c71736fc", size = 3988842, upload-time = "2025-09-24T13:50:37.478Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/704c7292f7014c7e74ec84eddb7b109e1fbae74a16deae9c1504b1d15565/shapely-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:980c777c612514c0cf99bc8a9de6d286f5e186dcaf9091252fcd444e5638193d", size = 4152714, upload-time = "2025-09-24T13:50:39.9Z" }, + { url = "https://files.pythonhosted.org/packages/53/46/319c9dc788884ad0785242543cdffac0e6530e4d0deb6c4862bc4143dcf3/shapely-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9111274b88e4d7b54a95218e243282709b330ef52b7b86bc6aaf4f805306f454", size = 1542745, upload-time = "2025-09-24T13:50:41.414Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bf/cb6c1c505cb31e818e900b9312d514f381fbfa5c4363edfce0fcc4f8c1a4/shapely-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:743044b4cfb34f9a67205cee9279feaf60ba7d02e69febc2afc609047cb49179", size = 1722861, upload-time = "2025-09-24T13:50:43.35Z" }, + { url = "https://files.pythonhosted.org/packages/c3/90/98ef257c23c46425dc4d1d31005ad7c8d649fe423a38b917db02c30f1f5a/shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b510dda1a3672d6879beb319bc7c5fd302c6c354584690973c838f46ec3e0fa8", size = 1832644, upload-time = "2025-09-24T13:50:44.886Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ab/0bee5a830d209adcd3a01f2d4b70e587cdd9fd7380d5198c064091005af8/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8cff473e81017594d20ec55d86b54bc635544897e13a7cfc12e36909c5309a2a", size = 1642887, upload-time = "2025-09-24T13:50:46.735Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5e/7d7f54ba960c13302584c73704d8c4d15404a51024631adb60b126a4ae88/shapely-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe7b77dc63d707c09726b7908f575fc04ff1d1ad0f3fb92aec212396bc6cfe5e", size = 2970931, upload-time = "2025-09-24T13:50:48.374Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a2/83fc37e2a58090e3d2ff79175a95493c664bcd0b653dd75cb9134645a4e5/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ed1a5bbfb386ee8332713bf7508bc24e32d24b74fc9a7b9f8529a55db9f4ee6", size = 3082855, upload-time = "2025-09-24T13:50:50.037Z" }, + { url = "https://files.pythonhosted.org/packages/44/2b/578faf235a5b09f16b5f02833c53822294d7f21b242f8e2d0cf03fb64321/shapely-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a84e0582858d841d54355246ddfcbd1fce3179f185da7470f41ce39d001ee1af", size = 3979960, upload-time = "2025-09-24T13:50:51.74Z" }, + { url = "https://files.pythonhosted.org/packages/4d/04/167f096386120f692cc4ca02f75a17b961858997a95e67a3cb6a7bbd6b53/shapely-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc3487447a43d42adcdf52d7ac73804f2312cbfa5d433a7d2c506dcab0033dfd", size = 4142851, upload-time = "2025-09-24T13:50:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/48/74/fb402c5a6235d1c65a97348b48cdedb75fb19eca2b1d66d04969fc1c6091/shapely-2.1.2-cp313-cp313-win32.whl", hash = "sha256:9c3a3c648aedc9f99c09263b39f2d8252f199cb3ac154fadc173283d7d111350", size = 1541890, upload-time = "2025-09-24T13:50:55.337Z" }, + { url = "https://files.pythonhosted.org/packages/41/47/3647fe7ad990af60ad98b889657a976042c9988c2807cf322a9d6685f462/shapely-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:ca2591bff6645c216695bdf1614fca9c82ea1144d4a7591a466fef64f28f0715", size = 1722151, upload-time = "2025-09-24T13:50:57.153Z" }, + { url = "https://files.pythonhosted.org/packages/3c/49/63953754faa51ffe7d8189bfbe9ca34def29f8c0e34c67cbe2a2795f269d/shapely-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2d93d23bdd2ed9dc157b46bc2f19b7da143ca8714464249bef6771c679d5ff40", size = 1834130, upload-time = "2025-09-24T13:50:58.49Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ee/dce001c1984052970ff60eb4727164892fb2d08052c575042a47f5a9e88f/shapely-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01d0d304b25634d60bd7cf291828119ab55a3bab87dc4af1e44b07fb225f188b", size = 1642802, upload-time = "2025-09-24T13:50:59.871Z" }, + { url = "https://files.pythonhosted.org/packages/da/e7/fc4e9a19929522877fa602f705706b96e78376afb7fad09cad5b9af1553c/shapely-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d8382dd120d64b03698b7298b89611a6ea6f55ada9d39942838b79c9bc89801", size = 3018460, upload-time = "2025-09-24T13:51:02.08Z" }, + { url = "https://files.pythonhosted.org/packages/a1/18/7519a25db21847b525696883ddc8e6a0ecaa36159ea88e0fef11466384d0/shapely-2.1.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19efa3611eef966e776183e338b2d7ea43569ae99ab34f8d17c2c054d3205cc0", size = 3095223, upload-time = "2025-09-24T13:51:04.472Z" }, + { url = "https://files.pythonhosted.org/packages/48/de/b59a620b1f3a129c3fecc2737104a0a7e04e79335bd3b0a1f1609744cf17/shapely-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:346ec0c1a0fcd32f57f00e4134d1200e14bf3f5ae12af87ba83ca275c502498c", size = 4030760, upload-time = "2025-09-24T13:51:06.455Z" }, + { url = "https://files.pythonhosted.org/packages/96/b3/c6655ee7232b417562bae192ae0d3ceaadb1cc0ffc2088a2ddf415456cc2/shapely-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6305993a35989391bd3476ee538a5c9a845861462327efe00dd11a5c8c709a99", size = 4170078, upload-time = "2025-09-24T13:51:08.584Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8e/605c76808d73503c9333af8f6cbe7e1354d2d238bda5f88eea36bfe0f42a/shapely-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:c8876673449f3401f278c86eb33224c5764582f72b653a415d0e6672fde887bf", size = 1559178, upload-time = "2025-09-24T13:51:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/36/f7/d317eb232352a1f1444d11002d477e54514a4a6045536d49d0c59783c0da/shapely-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:4a44bc62a10d84c11a7a3d7c1c4fe857f7477c3506e24c9062da0db0ae0c449c", size = 1739756, upload-time = "2025-09-24T13:51:12.105Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c4/3ce4c2d9b6aabd27d26ec988f08cb877ba9e6e96086eff81bfea93e688c7/shapely-2.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9a522f460d28e2bf4e12396240a5fc1518788b2fcd73535166d748399ef0c223", size = 1831290, upload-time = "2025-09-24T13:51:13.56Z" }, + { url = "https://files.pythonhosted.org/packages/17/b9/f6ab8918fc15429f79cb04afa9f9913546212d7fb5e5196132a2af46676b/shapely-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ff629e00818033b8d71139565527ced7d776c269a49bd78c9df84e8f852190c", size = 1641463, upload-time = "2025-09-24T13:51:14.972Z" }, + { url = "https://files.pythonhosted.org/packages/a5/57/91d59ae525ca641e7ac5551c04c9503aee6f29b92b392f31790fcb1a4358/shapely-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f67b34271dedc3c653eba4e3d7111aa421d5be9b4c4c7d38d30907f796cb30df", size = 2970145, upload-time = "2025-09-24T13:51:16.961Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cb/4948be52ee1da6927831ab59e10d4c29baa2a714f599f1f0d1bc747f5777/shapely-2.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21952dc00df38a2c28375659b07a3979d22641aeb104751e769c3ee825aadecf", size = 3073806, upload-time = "2025-09-24T13:51:18.712Z" }, + { url = "https://files.pythonhosted.org/packages/03/83/f768a54af775eb41ef2e7bec8a0a0dbe7d2431c3e78c0a8bdba7ab17e446/shapely-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1f2f33f486777456586948e333a56ae21f35ae273be99255a191f5c1fa302eb4", size = 3980803, upload-time = "2025-09-24T13:51:20.37Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/559c7c195807c91c79d38a1f6901384a2878a76fbdf3f1048893a9b7534d/shapely-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cf831a13e0d5a7eb519e96f58ec26e049b1fad411fc6fc23b162a7ce04d9cffc", size = 4133301, upload-time = "2025-09-24T13:51:21.887Z" }, + { url = "https://files.pythonhosted.org/packages/80/cd/60d5ae203241c53ef3abd2ef27c6800e21afd6c94e39db5315ea0cbafb4a/shapely-2.1.2-cp314-cp314-win32.whl", hash = "sha256:61edcd8d0d17dd99075d320a1dd39c0cb9616f7572f10ef91b4b5b00c4aeb566", size = 1583247, upload-time = "2025-09-24T13:51:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/74/d4/135684f342e909330e50d31d441ace06bf83c7dc0777e11043f99167b123/shapely-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:a444e7afccdb0999e203b976adb37ea633725333e5b119ad40b1ca291ecf311c", size = 1773019, upload-time = "2025-09-24T13:51:24.873Z" }, + { url = "https://files.pythonhosted.org/packages/a3/05/a44f3f9f695fa3ada22786dc9da33c933da1cbc4bfe876fe3a100bafe263/shapely-2.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5ebe3f84c6112ad3d4632b1fd2290665aa75d4cef5f6c5d77c4c95b324527c6a", size = 1834137, upload-time = "2025-09-24T13:51:26.665Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/4d57db45bf314573427b0a70dfca15d912d108e6023f623947fa69f39b72/shapely-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5860eb9f00a1d49ebb14e881f5caf6c2cf472c7fd38bd7f253bbd34f934eb076", size = 1642884, upload-time = "2025-09-24T13:51:28.029Z" }, + { url = "https://files.pythonhosted.org/packages/5a/27/4e29c0a55d6d14ad7422bf86995d7ff3f54af0eba59617eb95caf84b9680/shapely-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b705c99c76695702656327b819c9660768ec33f5ce01fa32b2af62b56ba400a1", size = 3018320, upload-time = "2025-09-24T13:51:29.903Z" }, + { url = "https://files.pythonhosted.org/packages/9f/bb/992e6a3c463f4d29d4cd6ab8963b75b1b1040199edbd72beada4af46bde5/shapely-2.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a1fd0ea855b2cf7c9cddaf25543e914dd75af9de08785f20ca3085f2c9ca60b0", size = 3094931, upload-time = "2025-09-24T13:51:32.699Z" }, + { url = "https://files.pythonhosted.org/packages/9c/16/82e65e21070e473f0ed6451224ed9fa0be85033d17e0c6e7213a12f59d12/shapely-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:df90e2db118c3671a0754f38e36802db75fe0920d211a27481daf50a711fdf26", size = 4030406, upload-time = "2025-09-24T13:51:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/7c/75/c24ed871c576d7e2b64b04b1fe3d075157f6eb54e59670d3f5ffb36e25c7/shapely-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:361b6d45030b4ac64ddd0a26046906c8202eb60d0f9f53085f5179f1d23021a0", size = 4169511, upload-time = "2025-09-24T13:51:36.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f7/b3d1d6d18ebf55236eec1c681ce5e665742aab3c0b7b232720a7d43df7b6/shapely-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:b54df60f1fbdecc8ebc2c5b11870461a6417b3d617f555e5033f1505d36e5735", size = 1602607, upload-time = "2025-09-24T13:51:37.757Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f6/f09272a71976dfc138129b8faf435d064a811ae2f708cb147dccdf7aacdb/shapely-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0036ac886e0923417932c2e6369b6c52e38e0ff5d9120b90eef5cd9a5fc5cae9", size = 1796682, upload-time = "2025-09-24T13:51:39.233Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +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 = "slicerator" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/52/f38586b82b2935f8b59a09b0a79c545a22ed062e728c9418bafeb51f61e0/slicerator-1.1.0.tar.gz", hash = "sha256:44010a7f5cd87680c07213b5cabe81d1fb71252962943e5373ee7d14605d6046", size = 38283, upload-time = "2022-04-07T18:54:08.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/ae/fa6cd331b364ad2bbc31652d025f5747d89cbb75576733dfdf8efe3e4d62/slicerator-1.1.0-py3-none-any.whl", hash = "sha256:167668d48c6d3a5ba0bd3d54b2688e81ee267dc20aef299e547d711e6f3c441a", size = 10274, upload-time = "2022-04-07T18:54:07.029Z" }, +] + +[[package]] +name = "spatial-image" +version = "1.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "xarray" }, + { name = "xarray-dataclass" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/e7/d82eec7ceb782e17d60df9b68c78b2e13e86b143c4225198b99cfed9650f/spatial_image-1.2.3.tar.gz", hash = "sha256:b0bb8ed84c9c11de00b3bbed7041375048327e0ffcb233f61aca7a24dcd75537", size = 32254, upload-time = "2025-08-08T17:11:18.065Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/5a/8ef888a4f56fa2ea5c10a7d6ff02286f503a93ea298bcaa9f51a41a20df8/spatial_image-1.2.3-py3-none-any.whl", hash = "sha256:b5280386a4d540c32c14cb4fdf74bd0242c99c168a7dac36204b29f8c27ce19a", size = 8737, upload-time = "2025-08-08T17:11:18.857Z" }, +] + +[[package]] +name = "spatialdata" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anndata" }, + { name = "annsel" }, + { name = "click" }, + { name = "dask" }, + { name = "dask-image" }, + { name = "datashader" }, + { name = "fsspec", extra = ["http", "s3"] }, + { name = "geopandas" }, + { name = "multiscale-spatial-image" }, + { name = "networkx" }, + { name = "numba" }, + { name = "numpy" }, + { name = "ome-zarr" }, + { name = "pandas" }, + { name = "pooch" }, + { name = "pyarrow" }, + { name = "rich" }, + { name = "scikit-image" }, + { name = "scipy" }, + { name = "setuptools" }, + { name = "shapely" }, + { name = "spatial-image" }, + { name = "typing-extensions" }, + { name = "universal-pathlib" }, + { name = "xarray" }, + { name = "xarray-schema" }, + { name = "xarray-spatial" }, + { name = "zarr" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/ae/0330a4bb68945fee2ae485a7318f51cc76911073189db4136effba023426/spatialdata-0.7.0.tar.gz", hash = "sha256:5a0a12d33e9682fe5193abe3430d8bfa106db62440423ae2d508d8a73ad264cc", size = 347530, upload-time = "2026-01-30T12:46:41.777Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/0e/4ed0def70624873f8a4feedd4183c1d090aab55e5d82f36fb3ddef4886cb/spatialdata-0.7.0-py3-none-any.whl", hash = "sha256:6df869089462994330e7b8d9b29dd2e3dbc02c09ce8d91b83a234911012ef3b0", size = 192054, upload-time = "2026-01-30T12:46:40.169Z" }, +] + +[[package]] +name = "spatialdata-test-fixtures-v0-7-0" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "spatialdata" }, +] + +[package.metadata] +requires-dist = [{ name = "spatialdata", specifier = "==0.7.0" }] + +[[package]] +name = "tifffile" +version = "2026.1.28" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/32/38498d2a1a5d70f33f6c3909bbad48557c9a54b0e33a9307ff06b6d416ba/tifffile-2026.1.28.tar.gz", hash = "sha256:537ae6466a8bb555c336108bb1878d8319d52c9c738041d3349454dea6956e1c", size = 374675, upload-time = "2026-01-29T05:17:24.992Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/19/529b28ca338c5a88315e71e672badc85eef89460c248c4164f6ce058f8c7/tifffile-2026.1.28-py3-none-any.whl", hash = "sha256:45b08a19cf603dd99952eff54a61519626a1912e4e2a4d355f05938fe4a6e9fd", size = 233011, upload-time = "2026-01-29T05:17:23.078Z" }, +] + +[[package]] +name = "toolz" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/d6/114b492226588d6ff54579d95847662fc69196bdeec318eb45393b24c192/toolz-1.1.0.tar.gz", hash = "sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b", size = 52613, upload-time = "2025-10-17T04:03:21.661Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl", hash = "sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8", size = 58093, upload-time = "2025-10-17T04:03:20.435Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + +[[package]] +name = "universal-pathlib" +version = "0.3.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fsspec" }, + { name = "pathlib-abc" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/ec/764b0d4593c6a8f5f66b347a19b5db9486dd0024b5e3339d468064a90c76/universal_pathlib-0.3.8.tar.gz", hash = "sha256:ead2b65bca3df6e11c3b7cb36fc9846340bc3c2db4ef57131550260422b0a3e8", size = 258837, upload-time = "2026-01-11T22:13:53.328Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/2c/fc9416619a418e94576aef84ef263906a24f76a21a1c3e96ddae25c82df9/universal_pathlib-0.3.8-py3-none-any.whl", hash = "sha256:dac4fd9a3df918d85bb6da678e794b5dfa9ecdb5ff74675b497553dbe50134b8", size = 82608, upload-time = "2026-01-11T22:13:51.313Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "wrapt" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2a/6de8a50cb435b7f42c46126cf1a54b2aab81784e74c8595c8e025e8f36d3/wrapt-2.0.1.tar.gz", hash = "sha256:9c9c635e78497cacb81e84f8b11b23e0aacac7a136e73b8e5b2109a1d9fc468f", size = 82040, upload-time = "2025-11-07T00:45:33.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/73/8cb252858dc8254baa0ce58ce382858e3a1cf616acebc497cb13374c95c6/wrapt-2.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1fdbb34da15450f2b1d735a0e969c24bdb8d8924892380126e2a293d9902078c", size = 78129, upload-time = "2025-11-07T00:43:48.852Z" }, + { url = "https://files.pythonhosted.org/packages/19/42/44a0db2108526ee6e17a5ab72478061158f34b08b793df251d9fbb9a7eb4/wrapt-2.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d32794fe940b7000f0519904e247f902f0149edbe6316c710a8562fb6738841", size = 61205, upload-time = "2025-11-07T00:43:50.402Z" }, + { url = "https://files.pythonhosted.org/packages/4d/8a/5b4b1e44b791c22046e90d9b175f9a7581a8cc7a0debbb930f81e6ae8e25/wrapt-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:386fb54d9cd903ee0012c09291336469eb7b244f7183d40dc3e86a16a4bace62", size = 61692, upload-time = "2025-11-07T00:43:51.678Z" }, + { url = "https://files.pythonhosted.org/packages/11/53/3e794346c39f462bcf1f58ac0487ff9bdad02f9b6d5ee2dc84c72e0243b2/wrapt-2.0.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7b219cb2182f230676308cdcacd428fa837987b89e4b7c5c9025088b8a6c9faf", size = 121492, upload-time = "2025-11-07T00:43:55.017Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7e/10b7b0e8841e684c8ca76b462a9091c45d62e8f2de9c4b1390b690eadf16/wrapt-2.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:641e94e789b5f6b4822bb8d8ebbdfc10f4e4eae7756d648b717d980f657a9eb9", size = 123064, upload-time = "2025-11-07T00:43:56.323Z" }, + { url = "https://files.pythonhosted.org/packages/0e/d1/3c1e4321fc2f5ee7fd866b2d822aa89b84495f28676fd976c47327c5b6aa/wrapt-2.0.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe21b118b9f58859b5ebaa4b130dee18669df4bd111daad082b7beb8799ad16b", size = 117403, upload-time = "2025-11-07T00:43:53.258Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b0/d2f0a413cf201c8c2466de08414a15420a25aa83f53e647b7255cc2fab5d/wrapt-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:17fb85fa4abc26a5184d93b3efd2dcc14deb4b09edcdb3535a536ad34f0b4dba", size = 121500, upload-time = "2025-11-07T00:43:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/bd/45/bddb11d28ca39970a41ed48a26d210505120f925918592283369219f83cc/wrapt-2.0.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b89ef9223d665ab255ae42cc282d27d69704d94be0deffc8b9d919179a609684", size = 116299, upload-time = "2025-11-07T00:43:58.877Z" }, + { url = "https://files.pythonhosted.org/packages/81/af/34ba6dd570ef7a534e7eec0c25e2615c355602c52aba59413411c025a0cb/wrapt-2.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a453257f19c31b31ba593c30d997d6e5be39e3b5ad9148c2af5a7314061c63eb", size = 120622, upload-time = "2025-11-07T00:43:59.962Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/693a13b4146646fb03254636f8bafd20c621955d27d65b15de07ab886187/wrapt-2.0.1-cp312-cp312-win32.whl", hash = "sha256:3e271346f01e9c8b1130a6a3b0e11908049fe5be2d365a5f402778049147e7e9", size = 58246, upload-time = "2025-11-07T00:44:03.169Z" }, + { url = "https://files.pythonhosted.org/packages/a7/36/715ec5076f925a6be95f37917b66ebbeaa1372d1862c2ccd7a751574b068/wrapt-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:2da620b31a90cdefa9cd0c2b661882329e2e19d1d7b9b920189956b76c564d75", size = 60492, upload-time = "2025-11-07T00:44:01.027Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3e/62451cd7d80f65cc125f2b426b25fbb6c514bf6f7011a0c3904fc8c8df90/wrapt-2.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:aea9c7224c302bc8bfc892b908537f56c430802560e827b75ecbde81b604598b", size = 58987, upload-time = "2025-11-07T00:44:02.095Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/41af4c46b5e498c90fc87981ab2972fbd9f0bccda597adb99d3d3441b94b/wrapt-2.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:47b0f8bafe90f7736151f61482c583c86b0693d80f075a58701dd1549b0010a9", size = 78132, upload-time = "2025-11-07T00:44:04.628Z" }, + { url = "https://files.pythonhosted.org/packages/1c/92/d68895a984a5ebbbfb175512b0c0aad872354a4a2484fbd5552e9f275316/wrapt-2.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cbeb0971e13b4bd81d34169ed57a6dda017328d1a22b62fda45e1d21dd06148f", size = 61211, upload-time = "2025-11-07T00:44:05.626Z" }, + { url = "https://files.pythonhosted.org/packages/e8/26/ba83dc5ae7cf5aa2b02364a3d9cf74374b86169906a1f3ade9a2d03cf21c/wrapt-2.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb7cffe572ad0a141a7886a1d2efa5bef0bf7fe021deeea76b3ab334d2c38218", size = 61689, upload-time = "2025-11-07T00:44:06.719Z" }, + { url = "https://files.pythonhosted.org/packages/cf/67/d7a7c276d874e5d26738c22444d466a3a64ed541f6ef35f740dbd865bab4/wrapt-2.0.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8d60527d1ecfc131426b10d93ab5d53e08a09c5fa0175f6b21b3252080c70a9", size = 121502, upload-time = "2025-11-07T00:44:09.557Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6b/806dbf6dd9579556aab22fc92908a876636e250f063f71548a8660382184/wrapt-2.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c654eafb01afac55246053d67a4b9a984a3567c3808bb7df2f8de1c1caba2e1c", size = 123110, upload-time = "2025-11-07T00:44:10.64Z" }, + { url = "https://files.pythonhosted.org/packages/e5/08/cdbb965fbe4c02c5233d185d070cabed2ecc1f1e47662854f95d77613f57/wrapt-2.0.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:98d873ed6c8b4ee2418f7afce666751854d6d03e3c0ec2a399bb039cd2ae89db", size = 117434, upload-time = "2025-11-07T00:44:08.138Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d1/6aae2ce39db4cb5216302fa2e9577ad74424dfbe315bd6669725569e048c/wrapt-2.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9e850f5b7fc67af856ff054c71690d54fa940c3ef74209ad9f935b4f66a0233", size = 121533, upload-time = "2025-11-07T00:44:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/79/35/565abf57559fbe0a9155c29879ff43ce8bd28d2ca61033a3a3dd67b70794/wrapt-2.0.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e505629359cb5f751e16e30cf3f91a1d3ddb4552480c205947da415d597f7ac2", size = 116324, upload-time = "2025-11-07T00:44:13.28Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e0/53ff5e76587822ee33e560ad55876d858e384158272cd9947abdd4ad42ca/wrapt-2.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2879af909312d0baf35f08edeea918ee3af7ab57c37fe47cb6a373c9f2749c7b", size = 120627, upload-time = "2025-11-07T00:44:14.431Z" }, + { url = "https://files.pythonhosted.org/packages/7c/7b/38df30fd629fbd7612c407643c63e80e1c60bcc982e30ceeae163a9800e7/wrapt-2.0.1-cp313-cp313-win32.whl", hash = "sha256:d67956c676be5a24102c7407a71f4126d30de2a569a1c7871c9f3cabc94225d7", size = 58252, upload-time = "2025-11-07T00:44:17.814Z" }, + { url = "https://files.pythonhosted.org/packages/85/64/d3954e836ea67c4d3ad5285e5c8fd9d362fd0a189a2db622df457b0f4f6a/wrapt-2.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:9ca66b38dd642bf90c59b6738af8070747b610115a39af2498535f62b5cdc1c3", size = 60500, upload-time = "2025-11-07T00:44:15.561Z" }, + { url = "https://files.pythonhosted.org/packages/89/4e/3c8b99ac93527cfab7f116089db120fef16aac96e5f6cdb724ddf286086d/wrapt-2.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:5a4939eae35db6b6cec8e7aa0e833dcca0acad8231672c26c2a9ab7a0f8ac9c8", size = 58993, upload-time = "2025-11-07T00:44:16.65Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f4/eff2b7d711cae20d220780b9300faa05558660afb93f2ff5db61fe725b9a/wrapt-2.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a52f93d95c8d38fed0669da2ebdb0b0376e895d84596a976c15a9eb45e3eccb3", size = 82028, upload-time = "2025-11-07T00:44:18.944Z" }, + { url = "https://files.pythonhosted.org/packages/0c/67/cb945563f66fd0f61a999339460d950f4735c69f18f0a87ca586319b1778/wrapt-2.0.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e54bbf554ee29fcceee24fa41c4d091398b911da6e7f5d7bffda963c9aed2e1", size = 62949, upload-time = "2025-11-07T00:44:20.074Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ca/f63e177f0bbe1e5cf5e8d9b74a286537cd709724384ff20860f8f6065904/wrapt-2.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:908f8c6c71557f4deaa280f55d0728c3bca0960e8c3dd5ceeeafb3c19942719d", size = 63681, upload-time = "2025-11-07T00:44:21.345Z" }, + { url = "https://files.pythonhosted.org/packages/39/a1/1b88fcd21fd835dca48b556daef750952e917a2794fa20c025489e2e1f0f/wrapt-2.0.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e2f84e9af2060e3904a32cea9bb6db23ce3f91cfd90c6b426757cf7cc01c45c7", size = 152696, upload-time = "2025-11-07T00:44:24.318Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/d9185500c1960d9f5f77b9c0b890b7fc62282b53af7ad1b6bd779157f714/wrapt-2.0.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3612dc06b436968dfb9142c62e5dfa9eb5924f91120b3c8ff501ad878f90eb3", size = 158859, upload-time = "2025-11-07T00:44:25.494Z" }, + { url = "https://files.pythonhosted.org/packages/91/60/5d796ed0f481ec003220c7878a1d6894652efe089853a208ea0838c13086/wrapt-2.0.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d2d947d266d99a1477cd005b23cbd09465276e302515e122df56bb9511aca1b", size = 146068, upload-time = "2025-11-07T00:44:22.81Z" }, + { url = "https://files.pythonhosted.org/packages/04/f8/75282dd72f102ddbfba137e1e15ecba47b40acff32c08ae97edbf53f469e/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7d539241e87b650cbc4c3ac9f32c8d1ac8a54e510f6dca3f6ab60dcfd48c9b10", size = 155724, upload-time = "2025-11-07T00:44:26.634Z" }, + { url = "https://files.pythonhosted.org/packages/5a/27/fe39c51d1b344caebb4a6a9372157bdb8d25b194b3561b52c8ffc40ac7d1/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:4811e15d88ee62dbf5c77f2c3ff3932b1e3ac92323ba3912f51fc4016ce81ecf", size = 144413, upload-time = "2025-11-07T00:44:27.939Z" }, + { url = "https://files.pythonhosted.org/packages/83/2b/9f6b643fe39d4505c7bf926d7c2595b7cb4b607c8c6b500e56c6b36ac238/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c1c91405fcf1d501fa5d55df21e58ea49e6b879ae829f1039faaf7e5e509b41e", size = 150325, upload-time = "2025-11-07T00:44:29.29Z" }, + { url = "https://files.pythonhosted.org/packages/bb/b6/20ffcf2558596a7f58a2e69c89597128781f0b88e124bf5a4cadc05b8139/wrapt-2.0.1-cp313-cp313t-win32.whl", hash = "sha256:e76e3f91f864e89db8b8d2a8311d57df93f01ad6bb1e9b9976d1f2e83e18315c", size = 59943, upload-time = "2025-11-07T00:44:33.211Z" }, + { url = "https://files.pythonhosted.org/packages/87/6a/0e56111cbb3320151eed5d3821ee1373be13e05b376ea0870711f18810c3/wrapt-2.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:83ce30937f0ba0d28818807b303a412440c4b63e39d3d8fc036a94764b728c92", size = 63240, upload-time = "2025-11-07T00:44:30.935Z" }, + { url = "https://files.pythonhosted.org/packages/1d/54/5ab4c53ea1f7f7e5c3e7c1095db92932cc32fd62359d285486d00c2884c3/wrapt-2.0.1-cp313-cp313t-win_arm64.whl", hash = "sha256:4b55cacc57e1dc2d0991dbe74c6419ffd415fb66474a02335cb10efd1aa3f84f", size = 60416, upload-time = "2025-11-07T00:44:32.002Z" }, + { url = "https://files.pythonhosted.org/packages/73/81/d08d83c102709258e7730d3cd25befd114c60e43ef3891d7e6877971c514/wrapt-2.0.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:5e53b428f65ece6d9dad23cb87e64506392b720a0b45076c05354d27a13351a1", size = 78290, upload-time = "2025-11-07T00:44:34.691Z" }, + { url = "https://files.pythonhosted.org/packages/f6/14/393afba2abb65677f313aa680ff0981e829626fed39b6a7e3ec807487790/wrapt-2.0.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ad3ee9d0f254851c71780966eb417ef8e72117155cff04821ab9b60549694a55", size = 61255, upload-time = "2025-11-07T00:44:35.762Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/a4a1f2fba205a9462e36e708ba37e5ac95f4987a0f1f8fd23f0bf1fc3b0f/wrapt-2.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7b822c61ed04ee6ad64bc90d13368ad6eb094db54883b5dde2182f67a7f22c0", size = 61797, upload-time = "2025-11-07T00:44:37.22Z" }, + { url = "https://files.pythonhosted.org/packages/12/db/99ba5c37cf1c4fad35349174f1e38bd8d992340afc1ff27f526729b98986/wrapt-2.0.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7164a55f5e83a9a0b031d3ffab4d4e36bbec42e7025db560f225489fa929e509", size = 120470, upload-time = "2025-11-07T00:44:39.425Z" }, + { url = "https://files.pythonhosted.org/packages/30/3f/a1c8d2411eb826d695fc3395a431757331582907a0ec59afce8fe8712473/wrapt-2.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e60690ba71a57424c8d9ff28f8d006b7ad7772c22a4af432188572cd7fa004a1", size = 122851, upload-time = "2025-11-07T00:44:40.582Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8d/72c74a63f201768d6a04a8845c7976f86be6f5ff4d74996c272cefc8dafc/wrapt-2.0.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3cd1a4bd9a7a619922a8557e1318232e7269b5fb69d4ba97b04d20450a6bf970", size = 117433, upload-time = "2025-11-07T00:44:38.313Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5a/df37cf4042cb13b08256f8e27023e2f9b3d471d553376616591bb99bcb31/wrapt-2.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b4c2e3d777e38e913b8ce3a6257af72fb608f86a1df471cb1d4339755d0a807c", size = 121280, upload-time = "2025-11-07T00:44:41.69Z" }, + { url = "https://files.pythonhosted.org/packages/54/34/40d6bc89349f9931e1186ceb3e5fbd61d307fef814f09fbbac98ada6a0c8/wrapt-2.0.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3d366aa598d69416b5afedf1faa539fac40c1d80a42f6b236c88c73a3c8f2d41", size = 116343, upload-time = "2025-11-07T00:44:43.013Z" }, + { url = "https://files.pythonhosted.org/packages/70/66/81c3461adece09d20781dee17c2366fdf0cb8754738b521d221ca056d596/wrapt-2.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c235095d6d090aa903f1db61f892fffb779c1eaeb2a50e566b52001f7a0f66ed", size = 119650, upload-time = "2025-11-07T00:44:44.523Z" }, + { url = "https://files.pythonhosted.org/packages/46/3a/d0146db8be8761a9e388cc9cc1c312b36d583950ec91696f19bbbb44af5a/wrapt-2.0.1-cp314-cp314-win32.whl", hash = "sha256:bfb5539005259f8127ea9c885bdc231978c06b7a980e63a8a61c8c4c979719d0", size = 58701, upload-time = "2025-11-07T00:44:48.277Z" }, + { url = "https://files.pythonhosted.org/packages/1a/38/5359da9af7d64554be63e9046164bd4d8ff289a2dd365677d25ba3342c08/wrapt-2.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:4ae879acc449caa9ed43fc36ba08392b9412ee67941748d31d94e3cedb36628c", size = 60947, upload-time = "2025-11-07T00:44:46.086Z" }, + { url = "https://files.pythonhosted.org/packages/aa/3f/96db0619276a833842bf36343685fa04f987dd6e3037f314531a1e00492b/wrapt-2.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:8639b843c9efd84675f1e100ed9e99538ebea7297b62c4b45a7042edb84db03e", size = 59359, upload-time = "2025-11-07T00:44:47.164Z" }, + { url = "https://files.pythonhosted.org/packages/71/49/5f5d1e867bf2064bf3933bc6cf36ade23505f3902390e175e392173d36a2/wrapt-2.0.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:9219a1d946a9b32bb23ccae66bdb61e35c62773ce7ca6509ceea70f344656b7b", size = 82031, upload-time = "2025-11-07T00:44:49.4Z" }, + { url = "https://files.pythonhosted.org/packages/2b/89/0009a218d88db66ceb83921e5685e820e2c61b59bbbb1324ba65342668bc/wrapt-2.0.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fa4184e74197af3adad3c889a1af95b53bb0466bced92ea99a0c014e48323eec", size = 62952, upload-time = "2025-11-07T00:44:50.74Z" }, + { url = "https://files.pythonhosted.org/packages/ae/18/9b968e920dd05d6e44bcc918a046d02afea0fb31b2f1c80ee4020f377cbe/wrapt-2.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c5ef2f2b8a53b7caee2f797ef166a390fef73979b15778a4a153e4b5fedce8fa", size = 63688, upload-time = "2025-11-07T00:44:52.248Z" }, + { url = "https://files.pythonhosted.org/packages/a6/7d/78bdcb75826725885d9ea26c49a03071b10c4c92da93edda612910f150e4/wrapt-2.0.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e042d653a4745be832d5aa190ff80ee4f02c34b21f4b785745eceacd0907b815", size = 152706, upload-time = "2025-11-07T00:44:54.613Z" }, + { url = "https://files.pythonhosted.org/packages/dd/77/cac1d46f47d32084a703df0d2d29d47e7eb2a7d19fa5cbca0e529ef57659/wrapt-2.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2afa23318136709c4b23d87d543b425c399887b4057936cd20386d5b1422b6fa", size = 158866, upload-time = "2025-11-07T00:44:55.79Z" }, + { url = "https://files.pythonhosted.org/packages/8a/11/b521406daa2421508903bf8d5e8b929216ec2af04839db31c0a2c525eee0/wrapt-2.0.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c72328f668cf4c503ffcf9434c2b71fdd624345ced7941bc6693e61bbe36bef", size = 146148, upload-time = "2025-11-07T00:44:53.388Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c0/340b272bed297baa7c9ce0c98ef7017d9c035a17a6a71dce3184b8382da2/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3793ac154afb0e5b45d1233cb94d354ef7a983708cc3bb12563853b1d8d53747", size = 155737, upload-time = "2025-11-07T00:44:56.971Z" }, + { url = "https://files.pythonhosted.org/packages/f3/93/bfcb1fb2bdf186e9c2883a4d1ab45ab099c79cbf8f4e70ea453811fa3ea7/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fec0d993ecba3991645b4857837277469c8cc4c554a7e24d064d1ca291cfb81f", size = 144451, upload-time = "2025-11-07T00:44:58.515Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6b/dca504fb18d971139d232652656180e3bd57120e1193d9a5899c3c0b7cdd/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:949520bccc1fa227274da7d03bf238be15389cd94e32e4297b92337df9b7a349", size = 150353, upload-time = "2025-11-07T00:44:59.753Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f6/a1de4bd3653afdf91d250ca5c721ee51195df2b61a4603d4b373aa804d1d/wrapt-2.0.1-cp314-cp314t-win32.whl", hash = "sha256:be9e84e91d6497ba62594158d3d31ec0486c60055c49179edc51ee43d095f79c", size = 60609, upload-time = "2025-11-07T00:45:03.315Z" }, + { url = "https://files.pythonhosted.org/packages/01/3a/07cd60a9d26fe73efead61c7830af975dfdba8537632d410462672e4432b/wrapt-2.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:61c4956171c7434634401db448371277d07032a81cc21c599c22953374781395", size = 64038, upload-time = "2025-11-07T00:45:00.948Z" }, + { url = "https://files.pythonhosted.org/packages/41/99/8a06b8e17dddbf321325ae4eb12465804120f699cd1b8a355718300c62da/wrapt-2.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:35cdbd478607036fee40273be8ed54a451f5f23121bd9d4be515158f9498f7ad", size = 60634, upload-time = "2025-11-07T00:45:02.087Z" }, + { url = "https://files.pythonhosted.org/packages/15/d1/b51471c11592ff9c012bd3e2f7334a6ff2f42a7aed2caffcf0bdddc9cb89/wrapt-2.0.1-py3-none-any.whl", hash = "sha256:4d2ce1bf1a48c5277d7969259232b57645aae5686dba1eaeade39442277afbca", size = 44046, upload-time = "2025-11-07T00:45:32.116Z" }, +] + +[[package]] +name = "xarray" +version = "2026.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/85/113ff1e2cde9e8a5b13c2f0ef4e9f5cd6ca3a036b6452f4dd523419289b5/xarray-2026.1.0.tar.gz", hash = "sha256:0c9814761f9d9a9545df37292d3fda89f83201f3e02ae0f09f03313d9cfdd5e2", size = 3107024, upload-time = "2026-01-28T17:49:03.822Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/8e/952a351c10df395d9bab850f611f4368834ae9104d6449049f5a49e00925/xarray-2026.1.0-py3-none-any.whl", hash = "sha256:5fcc03d3ed8dfb662aa254efe6cd65efc70014182bbc2126e4b90d291d970d41", size = 1403009, upload-time = "2026-01-28T17:49:01.538Z" }, +] + +[[package]] +name = "xarray-dataclass" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "typing-extensions" }, + { name = "xarray" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/99/04705ee6ff4d6d4db986058ceb19f2561014dde41e2f2ab73cc5ed35c958/xarray_dataclass-3.0.0.tar.gz", hash = "sha256:0fa438efda992b859dee07dc2f99c2660886402c366706c074c4c92795b354f1", size = 8376, upload-time = "2025-07-30T20:26:31.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/ea/bc1de04d06b7c59fc3ff647a11fa248bf80af5a6227647a31c6250c32ce6/xarray_dataclass-3.0.0-py3-none-any.whl", hash = "sha256:7956f29005671b2acaa0da700d8f359105dce84ef84f76ce2dd05b76dba3e826", size = 16862, upload-time = "2025-07-30T20:26:30.696Z" }, +] + +[[package]] +name = "xarray-schema" +version = "0.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "xarray" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/3d/e0652fad05696e34897f085d4c3115dc4bedb1fb8c1827d6c79ce74095a6/xarray-schema-0.0.3.tar.gz", hash = "sha256:9c6c760489c0690a70394b2ad1368b32f8fa1333911c361b4adf249384212920", size = 15010, upload-time = "2022-04-05T23:20:58.786Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/6d/f585a27b380ee987619b5617c0ca672a71a4345b67cfedbb6299750ce845/xarray_schema-0.0.3-py3-none-any.whl", hash = "sha256:aa6f856626b2e100213ba290407797464608b2555bb8e0b26093a97fe1ba38ce", size = 10019, upload-time = "2022-04-05T23:20:57.301Z" }, +] + +[[package]] +name = "xarray-spatial" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "datashader" }, + { name = "numba" }, + { name = "numpy" }, + { name = "xarray" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/ec/f9b23eb09bf35e38e6ef3a02874ea1ddf579eb365e2a492d29f78bcab12a/xarray_spatial-0.5.2.tar.gz", hash = "sha256:04ed5992eed25ad4d4a5277c73c1b64a3f677f179863e269fcdb199d26bcc2ea", size = 70254335, upload-time = "2025-12-18T20:09:59.099Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/d2/cb1c6104e6bf7ab77879fce7bc4bc6dcc4ae5a94246ab9c314b5d21022b2/xarray_spatial-0.5.2-py3-none-any.whl", hash = "sha256:9bf4850b7321d633f4178c5caea9cce7f52896b2533f1aa475ce5b187ac9cc4a", size = 2017596, upload-time = "2025-12-18T20:09:57.184Z" }, +] + +[[package]] +name = "yarl" +version = "1.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" }, + { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" }, + { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" }, + { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" }, + { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" }, + { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" }, + { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" }, + { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" }, + { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" }, + { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" }, + { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" }, + { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" }, + { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" }, + { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" }, + { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" }, + { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" }, + { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" }, + { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" }, + { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" }, + { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" }, + { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" }, + { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" }, + { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" }, + { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" }, + { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" }, + { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" }, + { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" }, + { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" }, + { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" }, + { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" }, + { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" }, + { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" }, + { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" }, + { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" }, + { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" }, + { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" }, + { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" }, + { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" }, + { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" }, + { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" }, + { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" }, + { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" }, + { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" }, + { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" }, + { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, +] + +[[package]] +name = "zarr" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "donfig" }, + { name = "google-crc32c" }, + { name = "numcodecs" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/76/7fa87f57c112c7b9c82f0a730f8b6f333e792574812872e2cd45ab604199/zarr-3.1.5.tar.gz", hash = "sha256:fbe0c79675a40c996de7ca08e80a1c0a20537bd4a9f43418b6d101395c0bba2b", size = 366825, upload-time = "2025-11-21T14:06:01.492Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/15/bb13b4913ef95ad5448490821eee4671d0e67673342e4d4070854e5fe081/zarr-3.1.5-py3-none-any.whl", hash = "sha256:29cd905afb6235b94c09decda4258c888fcb79bb6c862ef7c0b8fe009b5c8563", size = 284067, upload-time = "2025-11-21T14:05:59.235Z" }, +] diff --git a/scripts/validate-all.sh b/scripts/validate-all.sh index c2a6d05..d8b7565 100755 --- a/scripts/validate-all.sh +++ b/scripts/validate-all.sh @@ -27,9 +27,9 @@ mkdir -p "$RUN_DIR" echo -e "${GREEN}Output directory: $RUN_DIR${NC}" echo "" -# Step 1: Validate with Python (both versions) -echo -e "${YELLOW}Step 1/3: Validating with Python (v0.5.0 and v0.6.1)${NC}" -echo "This will test 10 datasets x 2 versions = 20 tests" +# Step 1: Validate with Python (all versions) +echo -e "${YELLOW}Step 1/3: Validating with Python (v0.5.0, v0.6.1, and v0.7.0)${NC}" +echo "This will test 10 datasets x 3 versions = 30 tests" echo "Note: Most time is spent importing spatialdata, not downloading" echo "Using parallel processing to speed things up..." echo "" @@ -98,7 +98,7 @@ PYTHON_TOTAL=$(jq 'length' "$RUN_DIR/python-results.json") JS_SUCCESS=$(jq '[.[] | select(.success == true)] | length' "$RUN_DIR/js-results.json") JS_TOTAL=$(jq 'length' "$RUN_DIR/js-results.json") -echo -e "Python (both versions): ${GREEN}${PYTHON_SUCCESS}/${PYTHON_TOTAL}${NC} successful" +echo -e "Python (all versions): ${GREEN}${PYTHON_SUCCESS}/${PYTHON_TOTAL}${NC} successful" echo -e "JavaScript: ${GREEN}${JS_SUCCESS}/${JS_TOTAL}${NC} successful" echo "" diff --git a/scripts/validate-datasets-js.js b/scripts/validate-datasets-js.js index 8ed5bfe..03799d4 100755 --- a/scripts/validate-datasets-js.js +++ b/scripts/validate-datasets-js.js @@ -126,8 +126,8 @@ function generateMarkdownTable(results, pythonResults = null) { // If we have Python results, show comparison if (pythonResults) { - lines.push('| Dataset | JS | Python v0.5.0 | Python v0.6.1 | URL |'); - lines.push('|---------|-------|---------------|---------------|-----|'); + lines.push('| Dataset | JS | Python v0.5.0 | Python v0.6.1 | Python v0.7.0 | URL |'); + lines.push('|---------|-------|---------------|---------------|---------------|-----|'); const pythonResultsByDataset = {}; for (const r of pythonResults) { @@ -141,11 +141,13 @@ function generateMarkdownTable(results, pythonResults = null) { const jsStatus = result.success ? '✅' : '❌'; const py050 = pythonResultsByDataset[result.datasetName]?.['0.5.0']; const py061 = pythonResultsByDataset[result.datasetName]?.['0.6.1']; + const py070 = pythonResultsByDataset[result.datasetName]?.['0.7.0']; const py050Status = py050 ? (py050.success ? '✅' : '❌') : '⏭️'; const py061Status = py061 ? (py061.success ? '✅' : '❌') : '⏭️'; + const py070Status = py070 ? (py070.success ? '✅' : '❌') : '⏭️'; const urlShort = result.datasetUrl.split('spatialdata-sandbox/')[1] || result.datasetUrl; - lines.push(`| ${result.datasetName} | ${jsStatus} | ${py050Status} | ${py061Status} | \`${urlShort}\` |`); + lines.push(`| ${result.datasetName} | ${jsStatus} | ${py050Status} | ${py061Status} | ${py070Status} | \`${urlShort}\` |`); } } else { lines.push('| Dataset | Status | URL |'); diff --git a/tests/integration/fixtures.test.ts b/tests/integration/fixtures.test.ts index bf295d8..b37bb5f 100644 --- a/tests/integration/fixtures.test.ts +++ b/tests/integration/fixtures.test.ts @@ -35,7 +35,7 @@ function ensureFixtures(version: string): string { } // Test matrix for different spatialdata versions -const versions = ['0.5.0', '0.6.1'] as const; +const versions = ['0.5.0', '0.6.1', '0.7.0'] as const; describe.each(versions)('Integration Tests - spatialdata v%s', (version) => { let fixturePath: string; @@ -132,9 +132,10 @@ describe.each(versions)('Integration Tests - spatialdata v%s', (version) => { }); describe('Fixture Generation', () => { - it('should generate fixtures for both versions', () => { + it('should generate fixtures for all versions', () => { const v050Path = join(projectRoot, 'test-fixtures', 'v0.5.0', 'blobs.zarr'); const v061Path = join(projectRoot, 'test-fixtures', 'v0.6.1', 'blobs.zarr'); + const v070Path = join(projectRoot, 'test-fixtures', 'v0.7.0', 'blobs.zarr'); // Try to generate if missing if (!existsSync(v050Path)) { @@ -147,10 +148,16 @@ describe('Fixture Generation', () => { } else { console.log('using existing fixture for 0.6.1'); } + if (!existsSync(v070Path)) { + ensureFixtures('0.7.0'); + } else { + console.log('using existing fixture for 0.7.0'); + } // Check that directories exist (even if generation failed, we want to know) expect(existsSync(join(projectRoot, 'test-fixtures', 'v0.5.0'))).toBe(true); expect(existsSync(join(projectRoot, 'test-fixtures', 'v0.6.1'))).toBe(true); - }, 60000); // 60 second timeout for generating both versions + expect(existsSync(join(projectRoot, 'test-fixtures', 'v0.7.0'))).toBe(true); + }, 90000); // 90 second timeout for generating all versions }); diff --git a/tests/unit/schemas.test.ts b/tests/unit/schemas.test.ts index 60ba980..10fbc0c 100644 --- a/tests/unit/schemas.test.ts +++ b/tests/unit/schemas.test.ts @@ -10,8 +10,10 @@ import { describe('Schema Transformations', () => { describe('rasterAttrsSchema - version normalization', () => { - it('should normalize v0.6.1 format (nested under ome) to internal format', () => { - const v061Format = { + it('should normalize OME-NGFF 0.5 format (nested under ome) to internal format', () => { + // Note: spatialdata_attrs.version is library version metadata (e.g., '0.6.1', '0.7.0') + // Format detection is STRUCTURAL (presence of 'ome' key), not based on version + const omeNgff05Format = { ome: { multiscales: [ { @@ -25,11 +27,11 @@ describe('Schema Transformations', () => { ], }, spatialdata_attrs: { - version: '0.6.1', + version: '0.6.1', // Library version metadata (doesn't control format detection) }, }; - const result = rasterAttrsSchema.parse(v061Format); + const result = rasterAttrsSchema.parse(omeNgff05Format); // Should have multiscales at top level after transformation expect(result.multiscales).toBeDefined(); @@ -39,8 +41,10 @@ describe('Schema Transformations', () => { expect('ome' in result).toBe(false); }); - it('should accept v0.5.0 format (top-level multiscales) as-is', () => { - const v050Format = { + it('should accept OME-NGFF 0.4 format (top-level multiscales) as-is', () => { + // Note: Format detection is STRUCTURAL (no 'ome' key = OME-NGFF 0.4) + // spatialdata_attrs.version is library version metadata, not format version + const omeNgff04Format = { multiscales: [ { name: 'test', @@ -52,11 +56,11 @@ describe('Schema Transformations', () => { }, ], spatialdata_attrs: { - version: '0.5.0', + version: '0.5.0', // Library version metadata (doesn't control format detection) }, }; - const result = rasterAttrsSchema.parse(v050Format); + const result = rasterAttrsSchema.parse(omeNgff04Format); // Should have multiscales at top level expect(result.multiscales).toBeDefined(); @@ -64,8 +68,9 @@ describe('Schema Transformations', () => { expect(result.multiscales[0].name).toBe('test'); }); - it('should preserve omero data from v0.6.1 format', () => { - const v061Format = { + it('should preserve omero data from OME-NGFF 0.5 format', () => { + // Format is determined by structure (presence of 'ome' key), not spatialdata_attrs.version + const omeNgff05Format = { ome: { multiscales: [ { @@ -87,11 +92,11 @@ describe('Schema Transformations', () => { }, }, spatialdata_attrs: { - version: '0.6.1', + version: '0.6.1', // Library version metadata }, }; - const result = rasterAttrsSchema.parse(v061Format); + const result = rasterAttrsSchema.parse(omeNgff05Format); expect(result.omero).toBeDefined(); expect(result.omero?.channels).toHaveLength(1); @@ -206,7 +211,7 @@ describe('Schema Transformations', () => { }, ], spatialdata_attrs: { - version: '0.6.1', + version: '0.6.1', // Library version metadata (for raster elements) }, }; @@ -235,7 +240,7 @@ describe('Schema Transformations', () => { }, ], spatialdata_attrs: { - version: '0.6.1', + version: '0.6.1', // Library version metadata (for raster elements) }, }; From 320fd99d90625112dfcd697feb56b64be5999b23 Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Fri, 30 Jan 2026 16:05:14 +0000 Subject: [PATCH 30/50] remove python datasets validation, manage js proxy locally in validation script. --- README.md | 94 ++++++--------------- package.json | 5 -- scripts/cors-proxy.js | 85 ++++++++++++++----- scripts/test-server.js | 2 +- scripts/validate-all.sh | 142 -------------------------------- scripts/validate-datasets-js.js | 102 +++++++++-------------- 6 files changed, 128 insertions(+), 302 deletions(-) delete mode 100755 scripts/validate-all.sh diff --git a/README.md b/README.md index f685894..1593525 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,8 @@ The server provides directory listings and serves all zarr metadata files with a The CORS proxy server allows accessing spatialdata stores that don't have CORS headers enabled. This is useful for local development when testing against remote stores. +**Standalone Usage:** + ```bash # Start the CORS proxy server (runs on http://localhost:8081) pnpm test:proxy @@ -191,9 +193,9 @@ pnpm test:proxy **Usage:** -Proxy a remote URL by encoding it directly in the path: +Proxy a remote URL using query parameter: ``` -http://localhost:8081/https://example.com/data.zarr/.zattrs +http://localhost:8081/?url=https://example.com/data.zarr/.zattrs ``` **Example:** @@ -206,53 +208,19 @@ import { readZarr } from '@spatialdata/core'; // Instead of: // const sdata = await readZarr('https://example.com/mydata.zarr'); -// Use the proxy (path-based form): -const sdata = await readZarr('http://localhost:8081/https://example.com/mydata.zarr'); +// Use the proxy (query parameter form): +const sdata = await readZarr('http://localhost:8081/?url=https://example.com/mydata.zarr'); ``` -**⚠️ Warning:** The CORS proxy is for local development only. It has no security restrictions and should never be exposed to the internet. - -### Dataset Validation - -**⚠️ Note, the results from this may indicate a number of failures in python due to not being able to open the remote stores.** - -The project includes scripts to validate dataset compatibility across different versions of the spatialdata library and the JavaScript implementation. - - -#### Validating with Python - -Test publicly available datasets with all Python versions (0.5.0, 0.6.1, and 0.7.0): - -```bash -# Validate all datasets with all Python versions -pnpm validate:datasets - -# Validate with a specific version only -pnpm validate:datasets:0.5.0 -pnpm validate:datasets:0.6.1 -pnpm validate:datasets:0.7.0 +**Automatic Proxy Management:** -# Validate a specific dataset -pnpm validate:datasets -- --dataset "Xenium" - -# Output to a file -pnpm validate:datasets -- --output-file validation-results.md +The validation script (`pnpm validate:datasets:js`) automatically manages its own proxy server. The proxy is started at the beginning of validation and stopped when complete, so you don't need to run it separately. -# Generate CSV output -pnpm validate:datasets -- --output-format csv --output-file results.csv - -# Generate JSON output (useful for programmatic comparison) -pnpm validate:datasets -- --output-format json --output-file results.json - -# Control parallel processing (default: 4 workers) -pnpm validate:datasets -- --workers 8 -pnpm validate:datasets -- --no-parallel # Sequential processing +**⚠️ Warning:** The CORS proxy is for local development only. It has no security restrictions and should never be exposed to the internet. -# Show detailed progress (verbose mode) -pnpm validate:datasets -- --verbose -``` +### Dataset Validation -**Performance Note:** The validation uses parallel processing by default (4 workers) to speed things up. Most time is spent importing the spatialdata library rather than downloading datasets. You can adjust the number of workers with `--workers N` or disable parallelism entirely with `--no-parallel`. +The project includes a script to validate dataset compatibility with the JavaScript implementation using publicly available spatialdata datasets. #### Validating with JavaScript @@ -268,22 +236,15 @@ pnpm validate:datasets:js # Validate a specific dataset pnpm validate:datasets:js -- --dataset "Xenium" -# Use the CORS proxy (make sure it's running first: pnpm test:proxy) -pnpm validate:datasets:js -- --use-proxy - # Output to a file pnpm validate:datasets:js -- --output-file validation-results-js.md - -# Compare with Python results -pnpm validate:datasets -- --output-format json --output-file python-results.json -pnpm validate:datasets:js -- --compare-python python-results.json --output-file comparison.md ``` -**Note:** The JavaScript validation may require the CORS proxy for datasets without CORS headers. Start it with `pnpm test:proxy` before running the validation. +**Note:** The validation script automatically starts its own CORS proxy server, uses it for all requests, and shuts it down when validation completes. You don't need to run `pnpm test:proxy` separately. #### Understanding the Results -The validation scripts generate a table showing which datasets work with each version: +The validation script generates a table showing which datasets work with the JavaScript implementation: - ✅ Success: Dataset loaded successfully - ❌ Failed: Dataset could not be loaded @@ -295,31 +256,24 @@ The detailed results include: - Error messages for failures This is useful for: -- Understanding baseline compatibility before testing the JS implementation -- Identifying version-specific issues +- Testing compatibility of the JavaScript implementation with real-world datasets +- Identifying issues with specific datasets - Tracking which datasets are known to work or fail -#### Comprehensive Validation Workflow +#### Output Formats -For a complete validation of all datasets across all implementations, use the all-in-one workflow: +The validation script supports multiple output formats: ```bash -# Run complete validation workflow -# This will: -# 1. Test all datasets with Python v0.5.0, v0.6.1, and v0.7.0 -# 2. Build the packages (if needed) -# 3. Test all datasets with JavaScript -# 4. Generate comparison reports -pnpm validate:all -``` +# Markdown (default) - Human-readable report +pnpm validate:datasets:js -- --output-format markdown --output-file results.md -Results are saved in `validation-results//` with: -- `python-results.md` - Python validation results -- `comparison-report.md` - Side-by-side comparison of Python and JS results -- `python-results.json` - Raw Python results (for programmatic use) -- `js-results.json` - Raw JavaScript results (for programmatic use) +# JSON - Machine-readable results +pnpm validate:datasets:js -- --output-format json --output-file results.json -A symlink `validation-results/latest/` always points to the most recent run. +# CSV - Spreadsheet-friendly format +pnpm validate:datasets:js -- --output-format csv --output-file results.csv +``` ## 📝 License diff --git a/package.json b/package.json index 04b87a9..eb2a2cc 100644 --- a/package.json +++ b/package.json @@ -20,12 +20,7 @@ "test:fixtures:generate:0.7.0": "uv run python/scripts/generate_fixtures.py --version 0.7.0", "test:server": "node scripts/test-server.js", "test:proxy": "node scripts/cors-proxy.js", - "validate:datasets": "uv run python/scripts/validate_datasets.py", - "validate:datasets:0.5.0": "uv run python/scripts/validate_datasets.py --version 0.5.0", - "validate:datasets:0.6.1": "uv run python/scripts/validate_datasets.py --version 0.6.1", - "validate:datasets:0.7.0": "uv run python/scripts/validate_datasets.py --version 0.7.0", "validate:datasets:js": "node scripts/validate-datasets-js.js", - "validate:all": "bash scripts/validate-all.sh", "lint": "biome check .", "format": "biome format --write .", "format:check": "biome format .", diff --git a/scripts/cors-proxy.js b/scripts/cors-proxy.js index 6bff183..de138c3 100755 --- a/scripts/cors-proxy.js +++ b/scripts/cors-proxy.js @@ -10,14 +10,15 @@ */ import { createServer } from 'node:http'; -import { URL } from 'node:url'; +import { URL, fileURLToPath } from 'node:url'; +import { resolve } from 'node:path'; const PORT = process.env.PORT || 8081; /** * Add CORS headers to a response */ -function addCorsHeaders(res) { +export function addCorsHeaders(res) { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS, POST, PUT, DELETE'); res.setHeader('Access-Control-Allow-Headers', '*'); @@ -28,7 +29,7 @@ function addCorsHeaders(res) { /** * Handle OPTIONS preflight requests */ -function handleOptions(req, res) { +export function handleOptions(req, res) { addCorsHeaders(res); res.writeHead(200); res.end(); @@ -37,7 +38,7 @@ function handleOptions(req, res) { /** * Proxy a request to the target URL */ -async function proxyRequest(req, res, targetUrl) { +export async function proxyRequest(req, res, targetUrl) { try { const url = new URL(targetUrl); @@ -121,7 +122,7 @@ async function proxyRequest(req, res, targetUrl) { /** * Handle HTTP request */ -async function handleRequest(req, res) { +export async function handleRequest(req, res) { // Handle OPTIONS preflight if (req.method === 'OPTIONS') { handleOptions(req, res); @@ -163,23 +164,69 @@ async function handleRequest(req, res) { } } -// Create and start server -const server = createServer(handleRequest); +/** + * Create a CORS proxy server + * @param {number} port - Port to listen on + * @returns {Promise} The HTTP server + */ +export function createProxyServer(port = 8081) { + const server = createServer(handleRequest); + + return new Promise((resolve, reject) => { + server.on('error', (err) => { + reject(err); + }); + + server.listen(port, () => { + resolve(server); + }); + }); +} + +/** + * Start a proxy server and return its base URL + * @param {number} port - Port to start on (will try next port if in use) + * @returns {Promise<{server: import('http').Server, baseUrl: string}>} + */ +export async function startProxyServer(port = 8081) { + try { + const server = await createProxyServer(port); + // Get the actual port (in case it was changed due to port conflict) + const actualPort = server.address()?.port || port; + const baseUrl = `http://localhost:${actualPort}`; + return { server, baseUrl }; + } catch (error) { + // If port is in use, try next port + if (error.code === 'EADDRINUSE') { + console.error(`Port ${port} is in use, trying ${port + 1}...`); + return startProxyServer(port + 1); + } + throw error; + } +} + +// If running as a standalone script, start the server +// Check if this file is being run directly (not imported) +const __filename = fileURLToPath(import.meta.url); +const isMainModule = process.argv[1] && resolve(process.argv[1]) === __filename; -server.listen(PORT, () => { - console.log(`CORS proxy server running at http://localhost:${PORT}`); +if (isMainModule) { + const server = await createProxyServer(PORT); + const actualPort = server.address()?.port || PORT; + + console.log(`CORS proxy server running at http://localhost:${actualPort}`); console.log('\nUsage:'); - console.log(` GET http://localhost:${PORT}/?url=`); - console.log(` Example: http://localhost:${PORT}/?url=https://example.com/data.zarr/.zattrs`); + console.log(` GET http://localhost:${actualPort}/?url=`); + console.log(` Example: http://localhost:${actualPort}/?url=https://example.com/data.zarr/.zattrs`); console.log('\nPress Ctrl+C to stop\n'); -}); -// Handle graceful shutdown -process.on('SIGINT', () => { - console.log('\nShutting down proxy server...'); - server.close(() => { - console.log('Proxy server stopped'); - process.exit(0); + // Handle graceful shutdown + process.on('SIGINT', () => { + console.log('\nShutting down proxy server...'); + server.close(() => { + console.log('Proxy server stopped'); + process.exit(0); + }); }); -}); +} diff --git a/scripts/test-server.js b/scripts/test-server.js index 99a1284..448bee4 100755 --- a/scripts/test-server.js +++ b/scripts/test-server.js @@ -161,7 +161,7 @@ server.listen(PORT, () => { console.log(`Test fixture server running at http://localhost:${PORT}`); console.log(`Serving fixtures from: ${fixturesDir}`); console.log(`\nAccess fixtures at: http://localhost:${PORT}/test-fixtures/`); - console.log(`Press Ctrl+C to stop\n`); + console.log('Press Ctrl+C to stop\n'); }); // Handle graceful shutdown diff --git a/scripts/validate-all.sh b/scripts/validate-all.sh deleted file mode 100755 index d8b7565..0000000 --- a/scripts/validate-all.sh +++ /dev/null @@ -1,142 +0,0 @@ -#!/bin/bash - -# Comprehensive dataset validation workflow -# This script runs all validation tests and generates a comparison report - -set -e # Exit on error - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Output directory -OUTPUT_DIR="validation-results" -TIMESTAMP=$(date +%Y%m%d-%H%M%S) -RUN_DIR="$OUTPUT_DIR/$TIMESTAMP" - -echo -e "${BLUE}================================${NC}" -echo -e "${BLUE}Dataset Validation Workflow${NC}" -echo -e "${BLUE}================================${NC}" -echo "" - -# Create output directory -mkdir -p "$RUN_DIR" -echo -e "${GREEN}Output directory: $RUN_DIR${NC}" -echo "" - -# Step 1: Validate with Python (all versions) -echo -e "${YELLOW}Step 1/3: Validating with Python (v0.5.0, v0.6.1, and v0.7.0)${NC}" -echo "This will test 10 datasets x 3 versions = 30 tests" -echo "Note: Most time is spent importing spatialdata, not downloading" -echo "Using parallel processing to speed things up..." -echo "" - -uv run python/scripts/validate_datasets.py \ - --output-format json \ - --output-file "$RUN_DIR/python-results.json" > "$RUN_DIR/python-output.txt" 2>&1 || { - echo -e "${RED}Python validation failed. Check $RUN_DIR/python-output.txt for details.${NC}" - exit 1 -} - -# Also generate markdown for Python results -uv run python/scripts/validate_datasets.py \ - --output-format markdown \ - --output-file "$RUN_DIR/python-results.md" > /dev/null 2>&1 - -echo -e "${GREEN}✓ Python validation complete${NC}" -echo "" - -# Step 2: Build the project if needed -if [ ! -d "packages/core/dist" ]; then - echo -e "${YELLOW}Step 2/3: Building packages${NC}" - echo "Building packages for JavaScript validation..." - pnpm build > "$RUN_DIR/build-output.txt" 2>&1 || { - echo -e "${RED}Build failed. Check $RUN_DIR/build-output.txt for details.${NC}" - exit 1 - } - echo -e "${GREEN}✓ Build complete${NC}" - echo "" -else - echo -e "${YELLOW}Step 2/3: Packages already built${NC}" - echo -e "${GREEN}✓ Skipping build${NC}" - echo "" -fi - -# Step 3: Validate with JavaScript -echo -e "${YELLOW}Step 3/3: Validating with JavaScript${NC}" -echo "Testing datasets with JS implementation..." -echo "" - -node scripts/validate-datasets-js.js \ - --output-format json \ - --output-file "$RUN_DIR/js-results.json" > "$RUN_DIR/js-output.txt" 2>&1 || { - echo -e "${RED}JavaScript validation failed. Check $RUN_DIR/js-output.txt for details.${NC}" - exit 1 -} - -# Generate comparison report -node scripts/validate-datasets-js.js \ - --compare-python "$RUN_DIR/python-results.json" \ - --output-format markdown \ - --output-file "$RUN_DIR/comparison-report.md" > /dev/null 2>&1 - -echo -e "${GREEN}✓ JavaScript validation complete${NC}" -echo "" - -# Generate summary -echo -e "${BLUE}================================${NC}" -echo -e "${BLUE}Validation Summary${NC}" -echo -e "${BLUE}================================${NC}" -echo "" - -# Count results from JSON files -PYTHON_SUCCESS=$(jq '[.[] | select(.success == true)] | length' "$RUN_DIR/python-results.json") -PYTHON_TOTAL=$(jq 'length' "$RUN_DIR/python-results.json") -JS_SUCCESS=$(jq '[.[] | select(.success == true)] | length' "$RUN_DIR/js-results.json") -JS_TOTAL=$(jq 'length' "$RUN_DIR/js-results.json") - -echo -e "Python (all versions): ${GREEN}${PYTHON_SUCCESS}/${PYTHON_TOTAL}${NC} successful" -echo -e "JavaScript: ${GREEN}${JS_SUCCESS}/${JS_TOTAL}${NC} successful" -echo "" - -echo -e "${BLUE}Generated Reports:${NC}" -echo -e " - Python results: ${RUN_DIR}/python-results.md" -echo -e " - JavaScript results: ${RUN_DIR}/comparison-report.md" -echo -e " - Raw JSON (Python): ${RUN_DIR}/python-results.json" -echo -e " - Raw JSON (JS): ${RUN_DIR}/js-results.json" -echo "" - -# Create a symlink to latest results -rm -f "$OUTPUT_DIR/latest" -ln -s "$TIMESTAMP" "$OUTPUT_DIR/latest" -echo -e "${GREEN}✓ Symlink created: $OUTPUT_DIR/latest${NC}" -echo "" - -# Display a preview of failures -PYTHON_FAILURES=$(jq -r '.[] | select(.success == false) | "\(.dataset_name) (v\(.spatialdata_version)): \(.error_type)"' "$RUN_DIR/python-results.json" | head -5) -JS_FAILURES=$(jq -r '.[] | select(.success == false) | "\(.datasetName): \(.errorType)"' "$RUN_DIR/js-results.json" | head -5) - -if [ -n "$PYTHON_FAILURES" ]; then - echo -e "${RED}Python Failures (showing first 5):${NC}" - echo "$PYTHON_FAILURES" - echo "" -fi - -if [ -n "$JS_FAILURES" ]; then - echo -e "${RED}JavaScript Failures (showing first 5):${NC}" - echo "$JS_FAILURES" - echo "" -fi - -echo -e "${GREEN}================================${NC}" -echo -e "${GREEN}Validation workflow complete!${NC}" -echo -e "${GREEN}================================${NC}" -echo "" -echo -e "View the comparison report:" -echo -e " cat $RUN_DIR/comparison-report.md" -echo "" -echo -e "Or open in your markdown viewer:" -echo -e " open $RUN_DIR/comparison-report.md" diff --git a/scripts/validate-datasets-js.js b/scripts/validate-datasets-js.js index 03799d4..fb3f25a 100755 --- a/scripts/validate-datasets-js.js +++ b/scripts/validate-datasets-js.js @@ -9,6 +9,7 @@ import { readZarr } from '../packages/core/dist/index.js'; import { writeFileSync } from 'node:fs'; import { resolve } from 'node:path'; +import { startProxyServer } from './cors-proxy.js'; // Dataset definitions from https://spatialdata.scverse.org/en/stable/tutorials/notebooks/datasets/README.html const DATASETS = [ @@ -57,7 +58,7 @@ const DATASETS = [ /** * Validate a single dataset */ -async function validateDataset(dataset, useProxy = false) { +async function validateDataset(dataset, proxyBaseUrl = null) { const result = { datasetName: dataset.name, datasetUrl: dataset.url, @@ -70,9 +71,9 @@ async function validateDataset(dataset, useProxy = false) { }; try { - // Use proxy if requested - const url = useProxy - ? `http://localhost:8081/?url=${encodeURIComponent(dataset.url)}` + // Use proxy if provided + const url = proxyBaseUrl + ? `${proxyBaseUrl}/?url=${encodeURIComponent(dataset.url)}` : dataset.url; // Try to read the dataset @@ -116,7 +117,7 @@ async function validateDataset(dataset, useProxy = false) { /** * Generate a markdown table from validation results */ -function generateMarkdownTable(results, pythonResults = null) { +function generateMarkdownTable(results) { const lines = []; lines.push('# SpatialData Dataset Compatibility Report (JavaScript)'); @@ -124,40 +125,13 @@ function generateMarkdownTable(results, pythonResults = null) { lines.push('## Summary'); lines.push(''); - // If we have Python results, show comparison - if (pythonResults) { - lines.push('| Dataset | JS | Python v0.5.0 | Python v0.6.1 | Python v0.7.0 | URL |'); - lines.push('|---------|-------|---------------|---------------|---------------|-----|'); + lines.push('| Dataset | Status | URL |'); + lines.push('|---------|--------|-----|'); - const pythonResultsByDataset = {}; - for (const r of pythonResults) { - if (!pythonResultsByDataset[r.dataset_name]) { - pythonResultsByDataset[r.dataset_name] = {}; - } - pythonResultsByDataset[r.dataset_name][r.spatialdata_version] = r; - } - - for (const result of results) { - const jsStatus = result.success ? '✅' : '❌'; - const py050 = pythonResultsByDataset[result.datasetName]?.['0.5.0']; - const py061 = pythonResultsByDataset[result.datasetName]?.['0.6.1']; - const py070 = pythonResultsByDataset[result.datasetName]?.['0.7.0']; - const py050Status = py050 ? (py050.success ? '✅' : '❌') : '⏭️'; - const py061Status = py061 ? (py061.success ? '✅' : '❌') : '⏭️'; - const py070Status = py070 ? (py070.success ? '✅' : '❌') : '⏭️'; - - const urlShort = result.datasetUrl.split('spatialdata-sandbox/')[1] || result.datasetUrl; - lines.push(`| ${result.datasetName} | ${jsStatus} | ${py050Status} | ${py061Status} | ${py070Status} | \`${urlShort}\` |`); - } - } else { - lines.push('| Dataset | Status | URL |'); - lines.push('|---------|--------|-----|'); - - for (const result of results) { - const status = result.success ? '✅' : '❌'; - const urlShort = result.datasetUrl.split('spatialdata-sandbox/')[1] || result.datasetUrl; - lines.push(`| ${result.datasetName} | ${status} | \`${urlShort}\` |`); - } + for (const result of results) { + const status = result.success ? '✅' : '❌'; + const urlShort = result.datasetUrl.split('spatialdata-sandbox/')[1] || result.datasetUrl; + lines.push(`| ${result.datasetName} | ${status} | \`${urlShort}\` |`); } lines.push(''); @@ -227,8 +201,6 @@ function parseArgs() { dataset: null, outputFormat: 'markdown', outputFile: null, - useProxy: false, - comparePython: null, }; for (let i = 2; i < process.argv.length; i++) { @@ -240,10 +212,6 @@ function parseArgs() { args.outputFormat = process.argv[++i]; } else if (arg === '--output-file' && i + 1 < process.argv.length) { args.outputFile = process.argv[++i]; - } else if (arg === '--use-proxy') { - args.useProxy = true; - } else if (arg === '--compare-python' && i + 1 < process.argv.length) { - args.comparePython = process.argv[++i]; } else if (arg === '--help') { console.log(` Usage: node validate-datasets-js.js [options] @@ -252,9 +220,9 @@ Options: --dataset Test only dataset matching this name --output-format Output format: markdown, csv, json (default: markdown) --output-file Write output to file instead of stdout - --use-proxy Use CORS proxy (http://localhost:8081) - --compare-python Compare with Python results JSON file --help Show this help message + +Note: The script automatically starts and manages a CORS proxy server for all requests. `); process.exit(0); } @@ -286,11 +254,19 @@ async function main() { } } - // Warn if using proxy - if (args.useProxy) { - console.error('Using CORS proxy at http://localhost:8081'); - console.error('Make sure the proxy is running: pnpm test:proxy'); + // Start proxy server automatically + console.error('Starting CORS proxy server...'); + let proxyServer = null; + let proxyBaseUrl = null; + try { + const proxy = await startProxyServer(); + proxyServer = proxy.server; + proxyBaseUrl = proxy.baseUrl; + console.error(`✓ Proxy server running at ${proxyBaseUrl}`); console.error(''); + } catch (error) { + console.error(`Failed to start proxy server: ${error.message}`); + process.exit(1); } // Run validation @@ -303,7 +279,7 @@ async function main() { current++; console.error(`[${current}/${datasets.length}] Testing ${dataset.name}...`); - const result = await validateDataset(dataset, args.useProxy); + const result = await validateDataset(dataset, proxyBaseUrl); results.push(result); const status = result.success ? '✅' : '❌'; @@ -315,23 +291,10 @@ async function main() { console.error('\nValidation complete!\n'); - // Load Python results if requested - let pythonResults = null; - if (args.comparePython) { - try { - const fs = await import('node:fs'); - const data = fs.readFileSync(args.comparePython, 'utf-8'); - pythonResults = JSON.parse(data); - console.error(`Loaded Python results from ${args.comparePython}\n`); - } catch (error) { - console.error(`Warning: Could not load Python results: ${error.message}`); - } - } - // Generate output let output; if (args.outputFormat === 'markdown') { - output = generateMarkdownTable(results, pythonResults); + output = generateMarkdownTable(results); } else if (args.outputFormat === 'csv') { // Simple CSV generation const lines = ['Dataset Name,Dataset URL,Implementation,Success,Error Type,Error Message,Elements,Coordinate Systems']; @@ -356,6 +319,15 @@ async function main() { } else { console.log(output); } + + // Shutdown proxy server + console.error('\nShutting down proxy server...'); + await new Promise((resolve) => { + proxyServer.close(() => { + console.error('✓ Proxy server stopped'); + resolve(); + }); + }); } main().catch(error => { From 53a638f91dca06a88fd6a54cb24824d988847b3f Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Fri, 30 Jan 2026 16:20:21 +0000 Subject: [PATCH 31/50] explicit behaviour for import.meta.env.DEV 'test' --- packages/core/src/models/VTableSource.ts | 7 ++----- packages/core/src/vite-env.d.ts | 2 ++ 2 files changed, 4 insertions(+), 5 deletions(-) create mode 100644 packages/core/src/vite-env.d.ts diff --git a/packages/core/src/models/VTableSource.ts b/packages/core/src/models/VTableSource.ts index 8242ec7..158033f 100644 --- a/packages/core/src/models/VTableSource.ts +++ b/packages/core/src/models/VTableSource.ts @@ -1,8 +1,5 @@ // this is a direct copy of the Vitessce implementation, with changes mostly to make it more normal TypeScript. -// and this for condtional version of parquet-wasm... -/// - import { tableFromIPC, type Table as ArrowTable } from 'apache-arrow'; import type { DataSourceParams } from '../Vutils'; import AnnDataSource from './VAnnDataSource'; @@ -23,8 +20,8 @@ async function getParquetModule() { // - probably ultimately may be using geoarrow-wasm / investigate deck.gl arrow layer // think about how that fits our 'core' (no deck deps) vs 'vis' structure etc. - // Check if we're in a browser/vite dev server environment - if (import.meta.env?.DEV) { + // Check if we're in a browser/vite dev server environment (but not in tests) + if (import.meta.env?.DEV && import.meta.env.MODE !== 'test') { // Use CDN version in vite dev server (workaround for module loading issues) // Reference: https://observablehq.com/@kylebarron/geoparquet-on-the-web console.warn( diff --git a/packages/core/src/vite-env.d.ts b/packages/core/src/vite-env.d.ts new file mode 100644 index 0000000..ed77210 --- /dev/null +++ b/packages/core/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// + From 65153f5def616e9a20769e6445a8dd86c67dfc31 Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Fri, 30 Jan 2026 16:40:06 +0000 Subject: [PATCH 32/50] Refactor parquet-wasm import to prioritize local module, with a fallback to CDN. --- packages/core/src/models/VTableSource.ts | 42 +++++++++++++++--------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/packages/core/src/models/VTableSource.ts b/packages/core/src/models/VTableSource.ts index 158033f..5cc56f4 100644 --- a/packages/core/src/models/VTableSource.ts +++ b/packages/core/src/models/VTableSource.ts @@ -20,26 +20,36 @@ async function getParquetModule() { // - probably ultimately may be using geoarrow-wasm / investigate deck.gl arrow layer // think about how that fits our 'core' (no deck deps) vs 'vis' structure etc. - // Check if we're in a browser/vite dev server environment (but not in tests) - if (import.meta.env?.DEV && import.meta.env.MODE !== 'test') { - // Use CDN version in vite dev server (workaround for module loading issues) + // Try local import first (works in Node.js, tests, and production builds) + try { + const module = await import('parquet-wasm'); + if (typeof module.default === 'function') { + await module.default(); + } + return { readParquet: module.readParquet, readSchema: module.readSchema }; + } catch (error) { + // Local import failed, try CDN fallback (needed in vite dev server) // Reference: https://observablehq.com/@kylebarron/geoparquet-on-the-web console.warn( - '[VTableSource] Using CDN version of parquet-wasm in vite dev server. ' + - 'This is a temporary workaround pending a better parquet module solution.' + '[VTableSource] Local parquet-wasm import failed, falling back to CDN version. ' + + 'This is a temporary workaround pending a better parquet module solution.', + error ); - // @ts-ignore - CDN import not recognized by TypeScript - const module = await import(/* webpackIgnore: true */ 'https://cdn.vitessce.io/parquet-wasm@2c23652/esm/parquet_wasm.js'); - await module.default(); - return { readParquet: module.readParquet, readSchema: module.readSchema }; - } - - // Use package import in Node.js or production builds - const module = await import('parquet-wasm'); - if (typeof module.default === 'function') { - await module.default(); + + try { + // @ts-ignore - CDN import not recognized by TypeScript + const cdnModule = await import('https://cdn.vitessce.io/parquet-wasm@2c23652/esm/parquet_wasm.js'); + await cdnModule.default(); + return { readParquet: cdnModule.readParquet, readSchema: cdnModule.readSchema }; + } catch (cdnError) { + // Both imports failed, throw an error + const localErrorMsg = error instanceof Error ? error.message : String(error); + const cdnErrorMsg = cdnError instanceof Error ? cdnError.message : String(cdnError); + throw new Error( + `Failed to load parquet-wasm from both local package and CDN. Local error: ${localErrorMsg}. CDN error: ${cdnErrorMsg}` + ); + } } - return { readParquet: module.readParquet, readSchema: module.readSchema }; } /** From 928e442f58fa8f39dc2353ecea83ddf20c8f87e2 Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Fri, 30 Jan 2026 17:01:56 +0000 Subject: [PATCH 33/50] coerce channel label to string to match internal use, interpret spec leniantly enough to accept number. --- packages/core/src/schemas/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/src/schemas/index.ts b/packages/core/src/schemas/index.ts index 0fd07c0..0a74ce4 100644 --- a/packages/core/src/schemas/index.ts +++ b/packages/core/src/schemas/index.ts @@ -202,7 +202,8 @@ const omeroSchema = z.object({ start: z.number(), }) .optional(), - label: z.union([z.number(), z.string()]).optional(), + // note - I think the schema says string but I encountered number in the wild. + label: z.coerce.string().optional(), family: z.string().optional(), color: z.string().optional(), active: z.boolean().optional(), From 8af5dfc5e575a29c8b9a2ebedfcb9ee78ace484a Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Fri, 30 Jan 2026 17:08:39 +0000 Subject: [PATCH 34/50] sanitise dataset['url'] and remove unneeded f-strings --- python/scripts/validate_datasets.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python/scripts/validate_datasets.py b/python/scripts/validate_datasets.py index ecf365a..974e088 100755 --- a/python/scripts/validate_datasets.py +++ b/python/scripts/validate_datasets.py @@ -101,7 +101,7 @@ def test_load(): try: print("Loading dataset...", file=sys.stderr, flush=True) # Try to read the dataset - sdata = sd.read_zarr("{dataset['url']}") + sdata = sd.read_zarr("{json.dumps(dataset['url'])}") # Extract basic info elements = {{}} @@ -257,7 +257,7 @@ def generate_markdown_table(results: list[ValidationResult]) -> str: lines.append("") if result.success: - lines.append(f"**Status:** ✅ Success") + lines.append("**Status:** ✅ Success") lines.append("") if result.elements: @@ -273,11 +273,11 @@ def generate_markdown_table(results: list[ValidationResult]) -> str: lines.append(f"**Coordinate Systems:** {', '.join(result.coordinate_systems)}") lines.append("") else: - lines.append(f"**Status:** ❌ Failed") + lines.append("**Status:** ❌ Failed") lines.append("") lines.append(f"**Error Type:** `{result.error_type}`") lines.append("") - lines.append(f"**Error Message:**") + lines.append("**Error Message:**") lines.append("```") lines.append(result.error_message or "No error message") lines.append("```") From c1512d49560294f18e02e63961195cdb6c692b98 Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Fri, 30 Jan 2026 17:39:44 +0000 Subject: [PATCH 35/50] change comment on open root in parseStoreContents --- packages/zarrextra/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/zarrextra/src/index.ts b/packages/zarrextra/src/index.ts index 0153aac..e931b01 100644 --- a/packages/zarrextra/src/index.ts +++ b/packages/zarrextra/src/index.ts @@ -40,8 +40,8 @@ async function parseStoreContents(store: IntermediateConsolidatedStore): Promise // Open root for resolving array paths later // this is throwing when I try to open http://localhost:8081/?url=https://s3.embl.de/spatialdata/spatialdata-sandbox/xenium_rep2_io.zarr // it gets to `open_group_v2`, `throw new NodeNotFoundError("v2 group", ...)` - // ... but also, if I open that sample with python spatialdata 0.6.1 or 0.5.0, it also fails, in different ways. - // thinking about compiling a table of results from the examples on the website... + // actually, multiple errors are thrown internally as it tries both v2 & v3 open, and we end up with a somewhat arbitrary error + // nb, downloading that xenium_rep2_io & opening from local server is ok. const root = await zarr.open(store, { kind: 'group' }); const tree: ZarrTree = {}; From 3e6f998580f7f7828e6c9fe3795624f08e5c7342 Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Fri, 30 Jan 2026 18:21:14 +0000 Subject: [PATCH 36/50] (llm) zod schema for zarr metadata --- packages/zarrextra/package.json | 3 +- packages/zarrextra/src/index.ts | 60 +++--- packages/zarrextra/src/zarrSchema.ts | 276 +++++++++++++++++++++++++++ pnpm-lock.yaml | 3 + 4 files changed, 317 insertions(+), 25 deletions(-) create mode 100644 packages/zarrextra/src/zarrSchema.ts diff --git a/packages/zarrextra/package.json b/packages/zarrextra/package.json index 22715a5..faf7f93 100644 --- a/packages/zarrextra/package.json +++ b/packages/zarrextra/package.json @@ -22,7 +22,8 @@ "test:coverage": "vitest run --coverage" }, "dependencies": { - "zarrita": "catalog:" + "zarrita": "catalog:", + "zod": "catalog:" }, "devDependencies": { "@types/node": "^22.10.5", diff --git a/packages/zarrextra/src/index.ts b/packages/zarrextra/src/index.ts index e931b01..47eb8f1 100644 --- a/packages/zarrextra/src/index.ts +++ b/packages/zarrextra/src/index.ts @@ -2,6 +2,7 @@ import * as zarr from 'zarrita'; import type { ZarrTree, ConsolidatedStore, ZAttrsAny, IntermediateConsolidatedStore, ZarrV2Metadata, ZarrV3Metadata, ZarrV3GroupNode, ZarrV3ArrayNode } from './types'; import { ATTRS_KEY, ZARRAY_KEY } from './types'; import { Err, Ok, type Result } from './result'; +import { validateAndConvertV2Zarray, validateV3Zarray } from './zarrSchema'; /** * As of this writing, this returns a nested object, leaf nodes have async functions that return the zarr array. @@ -107,6 +108,7 @@ async function parseStoreContents(store: IntermediateConsolidatedStore): Promise * Parse zarr v3 consolidated metadata from zarr.json * The actual zarr v3 structure has metadata nested under consolidated_metadata.metadata * We normalize it to have a top-level metadata field for internal use. + * Validates all array nodes in the metadata. */ async function parseZarrJson(zarrJson: unknown): Promise> { if (!zarrJson || typeof zarrJson !== 'object') { @@ -124,10 +126,25 @@ async function parseZarrJson(zarrJson: unknown): Promise; - metadata[path] = { - shape: (zarray.shape as number[]) || [], - data_type: (zarray.data_type as string) || 'float64', - chunk_grid: (zarray.chunk_grid as ZarrV3ArrayNode['chunk_grid']) || { - name: 'regular', - configuration: { chunk_shape: [] } - }, - chunk_key_encoding: (zarray.chunk_key_encoding as ZarrV3ArrayNode['chunk_key_encoding']) || { - name: 'default', - configuration: { separator: '/' } - }, - fill_value: (zarray.fill_value as number | string | boolean) || 0, - codecs: (zarray.codecs as ZarrV3ArrayNode['codecs']) || [], - attributes: (group.zattrs as Record) || {}, - dimension_names: (zarray.dimension_names as string[]) || [], - zarr_format: (zarray.zarr_format as number) || 3, - node_type: 'array', - storage_transformers: (zarray.storage_transformers as unknown[]) || [] - }; + // Array node - validate and convert v2 zarray metadata + try { + const arrayNode = validateAndConvertV2Zarray(group.zarray, path); + // Merge attributes from zattrs if present + if (group.zattrs && typeof group.zattrs === 'object') { + arrayNode.attributes = group.zattrs as Record; + } + metadata[path] = arrayNode; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to normalize v2 zarray metadata at path '${path}': ${errorMessage}`); + } } else { // Group node metadata[path] = { - attributes: (group.zattrs as Record) || {}, + attributes: (group.zattrs && typeof group.zattrs === 'object') + ? (group.zattrs as Record) + : {}, zarr_format: 3, consolidated_metadata: { kind: 'inline', @@ -244,7 +254,9 @@ async function tryConsolidated(store: zarr.FetchStore): Promise; + +/** + * Zod schema for v2 zarray metadata + * Handles both regular arrays and scalar arrays (empty shape/chunks) + */ +export const v2ZarraySchema = v2ZarrayBaseSchema.refine( + (data: V2ZarrayInput) => data.chunks.length === data.shape.length, + { + message: 'chunks length must match shape length', + path: ['chunks'] + } +).refine( + (data: V2ZarrayInput) => { + // For scalar arrays (empty shape/chunks), this check passes trivially + if (data.shape.length === 0) return true; + // For non-scalar arrays, each chunk must not exceed corresponding shape dimension + return data.chunks.every((chunk: number, i: number) => chunk <= data.shape[i]); + }, + { + message: 'each chunk dimension must not exceed corresponding shape dimension', + path: ['chunks'] + } +).refine( + (data: V2ZarrayInput) => !data.dimension_names || data.dimension_names.length === data.shape.length, + { + message: 'dimension_names length must match shape length', + path: ['dimension_names'] + } +); + +/** + * Base v3 zarray schema (before refines) + * Note: shape and chunk_shape can be empty arrays for scalar arrays (0-dimensional). + * In zarr, a scalar is represented as shape=[] and chunk_shape=[]. + */ +const v3ZarrayBaseSchema = z.object({ + shape: z.array(nonNegativeInteger), // Empty array allowed for scalars + data_type: z.string().min(1), + chunk_grid: v3ChunkGridSchema, + chunk_key_encoding: v3ChunkKeyEncodingSchema.optional(), + fill_value: z.union([z.number(), z.string(), z.boolean()]).optional(), + codecs: z.array(v3CodecSchema).optional().nullable(), + dimension_names: z.array(z.string()).optional().nullable(), + storage_transformers: z.array(z.unknown()).optional().nullable(), + zarr_format: z.number().optional(), + node_type: z.literal('array').optional() +}); + +type V3ZarrayInput = z.infer; + +/** + * Zod schema for v3 zarray metadata (for hybrid formats) + * Handles both regular arrays and scalar arrays (empty shape/chunk_shape) + */ +export const v3ZarraySchema = v3ZarrayBaseSchema.refine( + (data: V3ZarrayInput) => data.chunk_grid.configuration.chunk_shape.length === data.shape.length, + { + message: 'chunk_shape length must match shape length', + path: ['chunk_grid', 'configuration', 'chunk_shape'] + } +).refine( + (data: V3ZarrayInput) => { + // For scalar arrays (empty shape/chunk_shape), this check passes trivially + if (data.shape.length === 0) return true; + // For non-scalar arrays, each chunk must not exceed corresponding shape dimension + return data.chunk_grid.configuration.chunk_shape.every((chunk: number, i: number) => chunk <= data.shape[i]); + }, + { + message: 'each chunk dimension must not exceed corresponding shape dimension', + path: ['chunk_grid', 'configuration', 'chunk_shape'] + } +).refine( + (data: V3ZarrayInput) => !data.dimension_names || data.dimension_names.length === data.shape.length, + { + message: 'dimension_names length must match shape length', + path: ['dimension_names'] + } +); + +/** + * Validate and convert v2 zarray metadata to v3 format + * Handles both pure v2 format (with chunks, dtype) and hybrid formats + * @throws Error if required fields are missing or invalid + */ +export function validateAndConvertV2Zarray( + zarray: unknown, + path: string +): ZarrV3ArrayNode { + const obj = zarray as Record; + + // Check if this already has v3 fields (hybrid/partially converted format) + const hasV3Fields = 'data_type' in obj && 'chunk_grid' in obj; + const hasV2Fields = 'shape' in obj && 'chunks' in obj && 'dtype' in obj; + + if (hasV3Fields && !hasV2Fields) { + // Already in v3 format - validate v3 fields + return validateV3Zarray(zarray, path); + } + + // Validate v2 format + const parseResult = v2ZarraySchema.safeParse(zarray); + if (!parseResult.success) { + const errors = parseResult.error.issues.map((e: z.ZodIssue) => { + const pathStr = e.path.length > 0 ? `'${e.path.join('.')}'` : ''; + return `${pathStr}${pathStr ? ': ' : ''}${e.message}`; + }).join('; '); + throw new Error(`Invalid .zarray metadata at path '${path}': ${errors}`); + } + + const v2 = parseResult.data; + + // Convert v2 compressor and filters to v3 codecs + const codecs: ZarrV3ArrayNode['codecs'] = []; + + // Add filters first (if present) + if (v2.filters) { + for (const filter of v2.filters) { + codecs.push({ + name: filter.id, + configuration: filter.configuration + }); + } + } + + // Add compressor (if present and not null) + if (v2.compressor) { + codecs.push({ + name: v2.compressor.id, + configuration: v2.compressor.configuration + }); + } + + // Build v3 array node + return { + shape: v2.shape, + data_type: v2.dtype, + chunk_grid: { + name: 'regular', + configuration: { chunk_shape: v2.chunks } + }, + chunk_key_encoding: { + name: 'default', + configuration: { separator: '/' } + }, + fill_value: v2.fill_value ?? 0, + codecs, + attributes: {}, + dimension_names: v2.dimension_names ?? [], + zarr_format: 3, + node_type: 'array', + storage_transformers: [] + }; +} + +/** + * Validate v3 zarray metadata (for hybrid formats that already have v3 fields) + * @throws Error if required fields are missing or invalid + */ +export function validateV3Zarray( + zarray: unknown, + path: string +): ZarrV3ArrayNode { + const parseResult = v3ZarraySchema.safeParse(zarray); + if (!parseResult.success) { + const errors = parseResult.error.issues.map((e: z.ZodIssue) => { + const pathStr = e.path.length > 0 ? `'${e.path.join('.')}'` : ''; + return `${pathStr}${pathStr ? ': ' : ''}${e.message}`; + }).join('; '); + throw new Error(`Invalid .zarray metadata at path '${path}': ${errors}`); + } + + const v3 = parseResult.data; + + return { + shape: v3.shape, + data_type: v3.data_type, + chunk_grid: v3.chunk_grid, + chunk_key_encoding: v3.chunk_key_encoding ?? { + name: 'default', + configuration: { separator: '/' } + }, + fill_value: v3.fill_value ?? 0, + codecs: v3.codecs ?? [], + attributes: {}, + dimension_names: v3.dimension_names ?? [], + zarr_format: 3, + node_type: 'array', + storage_transformers: v3.storage_transformers ?? [] + }; +} + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bfe6a79..72131d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -303,6 +303,9 @@ importers: zarrita: specifier: 'catalog:' version: 0.5.4 + zod: + specifier: 'catalog:' + version: 4.1.13 devDependencies: '@types/node': specifier: ^22.10.5 From 760afd1cdc6a1fe510741aadcdd6c87af9cdd676 Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Fri, 30 Jan 2026 18:51:05 +0000 Subject: [PATCH 37/50] (llm) rearrange tests into packages, each with at least some minimal test. --- package.json | 2 +- packages/core/src/index.test.ts | 49 -- .../core/tests/schemas.spec.ts | 68 ++- packages/core/vite.config.ts | 1 + packages/react/package.json | 1 + packages/react/tests/index.spec.tsx | 28 ++ packages/react/vite.config.ts | 11 +- packages/vis/package.json | 1 + packages/vis/tests/index.spec.tsx | 43 ++ packages/vis/vite.config.ts | 11 +- packages/zarrextra/tests/zarrSchema.spec.ts | 437 ++++++++++++++++++ packages/zarrextra/vite.config.ts | 1 + pnpm-lock.yaml | 297 +++++++++++- pnpm-workspace.yaml | 1 + vitest.config.ts | 6 +- 15 files changed, 887 insertions(+), 70 deletions(-) delete mode 100644 packages/core/src/index.test.ts rename tests/unit/schemas.test.ts => packages/core/tests/schemas.spec.ts (83%) create mode 100644 packages/react/tests/index.spec.tsx create mode 100644 packages/vis/tests/index.spec.tsx create mode 100644 packages/zarrextra/tests/zarrSchema.spec.ts diff --git a/package.json b/package.json index eb2a2cc..3b228b0 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "scripts": { "build": "pnpm -r --filter='!docs' run build", "test": "pnpm -r --filter='!docs' run test", - "test:unit": "vitest run tests/unit", + "test:unit": "vitest run", "test:integration": "vitest run tests/integration", "test:all": "pnpm test:unit && pnpm test:integration", "test:fixtures:generate": "uv run python/scripts/generate_fixtures.py", diff --git a/packages/core/src/index.test.ts b/packages/core/src/index.test.ts deleted file mode 100644 index 26e88ec..0000000 --- a/packages/core/src/index.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { coordinateTransformationSchema, spatialDataSchema } from './schemas/index.js'; - -describe('SpatialData Core', () => { - describe('schemas', () => { - it('should validate coordinate transformation', () => { - // coordinateTransformationSchema expects an array of transformations - const validTransforms = [ - { - type: 'affine', - affine: [ - [1, 0, 0], - [0, 1, 0], - ], - }, - ]; - - expect(() => coordinateTransformationSchema.parse(validTransforms)).not.toThrow(); - }); - - it('should validate spatial data metadata', () => { - // coordinateSystems values are arrays of transformations - const validMetadata = { - version: '0.1.0', - coordinateSystems: { - global: [ - { - type: 'affine', - affine: [ - [1, 0, 0], - [0, 1, 0], - ], - }, - ], - }, - }; - - expect(() => spatialDataSchema.parse(validMetadata)).not.toThrow(); - }); - - it('should reject invalid spatial data metadata', () => { - const invalidMetadata = { - version: 123, // should be string - }; - - expect(() => spatialDataSchema.parse(invalidMetadata)).toThrow(); - }); - }); -}); diff --git a/tests/unit/schemas.test.ts b/packages/core/tests/schemas.spec.ts similarity index 83% rename from tests/unit/schemas.test.ts rename to packages/core/tests/schemas.spec.ts index 10fbc0c..7fbdff3 100644 --- a/tests/unit/schemas.test.ts +++ b/packages/core/tests/schemas.spec.ts @@ -6,10 +6,11 @@ import { pointsAttrsSchema, tableAttrsSchema, spatialDataSchema, -} from '../../packages/core/src/schemas/index.js'; +} from '../src/schemas/index.js'; describe('Schema Transformations', () => { describe('rasterAttrsSchema - version normalization', () => { + //TODO: invert this type... it('should normalize OME-NGFF 0.5 format (nested under ome) to internal format', () => { // Note: spatialdata_attrs.version is library version metadata (e.g., '0.6.1', '0.7.0') // Format detection is STRUCTURAL (presence of 'ome' key), not based on version @@ -121,7 +122,7 @@ describe('Schema Transformations', () => { it('should validate translation transformation', () => { const transform = [ { - type: 'translation' as const, + type: 'translation', translation: [10.0, 20.0], }, ]; @@ -132,7 +133,7 @@ describe('Schema Transformations', () => { it('should validate affine transformation', () => { const transform = [ { - type: 'affine' as const, + type: 'affine', affine: [ [1, 0, 0], [0, 1, 0], @@ -147,7 +148,7 @@ describe('Schema Transformations', () => { it('should validate identity transformation', () => { const transform = [ { - type: 'identity' as const, + type: 'identity', }, ]; @@ -157,10 +158,10 @@ describe('Schema Transformations', () => { it('should validate sequence transformation', () => { const transform = [ { - type: 'sequence' as const, + type: 'sequence', transformations: [ - { type: 'scale' as const, scale: [2.0, 2.0] }, - { type: 'translation' as const, translation: [10.0, 10.0] }, + { type: 'scale', scale: [2.0, 2.0] }, + { type: 'translation', translation: [10.0, 10.0] }, ], }, ]; @@ -171,13 +172,13 @@ describe('Schema Transformations', () => { it('should validate transformations with coordinate system references', () => { const transform = [ { - type: 'scale' as const, + type: 'scale', scale: [1.0, 2.0], input: { name: 'input_cs', axes: [ - { name: 'x', type: 'space' as const }, - { name: 'y', type: 'space' as const }, + { name: 'x', type: 'space' }, + { name: 'y', type: 'space' }, ], }, output: { @@ -197,6 +198,22 @@ describe('Schema Transformations', () => { expect(() => coordinateTransformationSchema.parse(transform)).toThrow(); }); + + // Tests from packages/core/src/index.test.ts + it('should validate coordinate transformation (from index.test.ts)', () => { + // coordinateTransformationSchema expects an array of transformations + const validTransforms = [ + { + type: 'affine', + affine: [ + [1, 0, 0], + [0, 1, 0], + ], + }, + ]; + + expect(() => coordinateTransformationSchema.parse(validTransforms)).not.toThrow(); + }); }); describe('shapesAttrsSchema', () => { @@ -206,7 +223,7 @@ describe('Schema Transformations', () => { axes: ['x', 'y'], coordinateTransformations: [ { - type: 'scale' as const, + type: 'scale', scale: [1.0, 1.0], }, ], @@ -303,6 +320,35 @@ describe('Schema Transformations', () => { expect(() => spatialDataSchema.parse(metadata)).toThrow(); }); + + // Tests from packages/core/src/index.test.ts + it('should validate spatial data metadata (from index.test.ts)', () => { + // coordinateSystems values are arrays of transformations + const validMetadata = { + version: '0.1.0', + coordinateSystems: { + global: [ + { + type: 'affine', + affine: [ + [1, 0, 0], + [0, 1, 0], + ], + }, + ], + }, + }; + + expect(() => spatialDataSchema.parse(validMetadata)).not.toThrow(); + }); + + it('should reject invalid spatial data metadata (from index.test.ts)', () => { + const invalidMetadata = { + version: 123, // should be string + }; + + expect(() => spatialDataSchema.parse(invalidMetadata)).toThrow(); + }); }); }); diff --git a/packages/core/vite.config.ts b/packages/core/vite.config.ts index c26260c..6c055f5 100644 --- a/packages/core/vite.config.ts +++ b/packages/core/vite.config.ts @@ -18,6 +18,7 @@ export default defineConfig({ test: { globals: true, environment: 'node', + include: ['tests/**/*.spec.ts'], coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], diff --git a/packages/react/package.json b/packages/react/package.json index d31a635..0bcbf20 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -33,6 +33,7 @@ "@types/react": "catalog:", "@types/react-dom": "catalog:", "@vitejs/plugin-react": "catalog:", + "jsdom": "catalog:", "typescript": "catalog:", "vite": "catalog:", "vite-plugin-dts": "catalog:", diff --git a/packages/react/tests/index.spec.tsx b/packages/react/tests/index.spec.tsx new file mode 100644 index 0000000..f8b7718 --- /dev/null +++ b/packages/react/tests/index.spec.tsx @@ -0,0 +1,28 @@ +import { describe, expect, it } from 'vitest'; +import * as ReactExports from '../src/index.js'; + +describe('@spatialdata/react', () => { + it('should export useSpatialData hook', () => { + expect(ReactExports.useSpatialData).toBeDefined(); + expect(typeof ReactExports.useSpatialData).toBe('function'); + }); + + it('should export SpatialDataProvider component', () => { + expect(ReactExports.SpatialDataProvider).toBeDefined(); + expect(typeof ReactExports.SpatialDataProvider).toBe('function'); + }); + + it('should export useSpatialDataContext hook', () => { + expect(ReactExports.useSpatialDataContext).toBeDefined(); + expect(typeof ReactExports.useSpatialDataContext).toBe('function'); + }); + + it('should have all expected exports', () => { + const exports = Object.keys(ReactExports); + expect(exports.length).toBeGreaterThan(0); + expect(exports).toContain('useSpatialData'); + expect(exports).toContain('SpatialDataProvider'); + expect(exports).toContain('useSpatialDataContext'); + }); +}); + diff --git a/packages/react/vite.config.ts b/packages/react/vite.config.ts index 54da95d..d499c26 100644 --- a/packages/react/vite.config.ts +++ b/packages/react/vite.config.ts @@ -1,10 +1,19 @@ import { fileURLToPath } from 'node:url'; import { defineViteConfig } from '../../vite.config.base'; +import { mergeConfig } from 'vite'; const pkgRoot = fileURLToPath(new URL('.', import.meta.url)); -export default defineViteConfig({ +const baseConfig = defineViteConfig({ pkgRoot, libName: 'SpatialDataReact', external: ['@spatialdata/core'], }); + +export default mergeConfig(baseConfig, { + test: { + globals: true, + environment: 'jsdom', + include: ['tests/**/*.spec.ts*'], + }, +}); diff --git a/packages/vis/package.json b/packages/vis/package.json index ed4a8fc..d879488 100644 --- a/packages/vis/package.json +++ b/packages/vis/package.json @@ -45,6 +45,7 @@ "@types/react": "catalog:", "@types/react-dom": "catalog:", "@vitejs/plugin-react": "catalog:", + "jsdom": "catalog:", "typescript": "catalog:", "vite": "catalog:", "vite-plugin-dts": "catalog:", diff --git a/packages/vis/tests/index.spec.tsx b/packages/vis/tests/index.spec.tsx new file mode 100644 index 0000000..60cf7cb --- /dev/null +++ b/packages/vis/tests/index.spec.tsx @@ -0,0 +1,43 @@ +import { describe, expect, it } from 'vitest'; +import * as VisExports from '../src/index.js'; + +describe('@spatialdata/vis', () => { + it('should export SpatialCanvas component', () => { + // SpatialCanvas is exported as a named export, not default + expect(VisExports.SpatialCanvas).toBeDefined(); + expect(typeof VisExports.SpatialCanvas).toBe('function'); + }); + + it('should export named components', () => { + expect(VisExports.Sketch).toBeDefined(); + expect(VisExports.SpatialDataTree).toBeDefined(); + expect(VisExports.Transforms).toBeDefined(); + expect(VisExports.ImageView).toBeDefined(); + expect(VisExports.Shapes).toBeDefined(); + expect(VisExports.Table).toBeDefined(); + }); + + it('should export SpatialCanvas hooks and utilities', () => { + expect(VisExports.SpatialCanvasProvider).toBeDefined(); + expect(VisExports.useSpatialCanvasStore).toBeDefined(); + expect(VisExports.useSpatialCanvasActions).toBeDefined(); + expect(VisExports.useSpatialCanvasStoreApi).toBeDefined(); + expect(VisExports.createSpatialCanvasStore).toBeDefined(); + expect(VisExports.useSpatialViewState).toBeDefined(); + expect(VisExports.useViewStateUrl).toBeDefined(); + }); + + it('should have all expected exports', () => { + const exports = Object.keys(VisExports); + expect(exports.length).toBeGreaterThan(0); + // Check for key exports + expect(exports).toContain('Sketch'); + expect(exports).toContain('SpatialDataTree'); + expect(exports).toContain('Transforms'); + expect(exports).toContain('ImageView'); + expect(exports).toContain('Shapes'); + expect(exports).toContain('Table'); + expect(exports).toContain('SpatialCanvasProvider'); + }); +}); + diff --git a/packages/vis/vite.config.ts b/packages/vis/vite.config.ts index 8aa31a0..b9b812d 100644 --- a/packages/vis/vite.config.ts +++ b/packages/vis/vite.config.ts @@ -1,10 +1,19 @@ import { fileURLToPath } from 'node:url'; import { defineViteConfig } from '../../vite.config.base'; +import { mergeConfig } from 'vite'; const pkgRoot = fileURLToPath(new URL('.', import.meta.url)); -export default defineViteConfig({ +const baseConfig = defineViteConfig({ pkgRoot, libName: 'SpatialDataVis', external: ['@spatialdata/core', '@spatialdata/react'], }); + +export default mergeConfig(baseConfig, { + test: { + globals: true, + environment: 'jsdom', + include: ['tests/**/*.spec.ts*'], + }, +}); diff --git a/packages/zarrextra/tests/zarrSchema.spec.ts b/packages/zarrextra/tests/zarrSchema.spec.ts new file mode 100644 index 0000000..6907814 --- /dev/null +++ b/packages/zarrextra/tests/zarrSchema.spec.ts @@ -0,0 +1,437 @@ +import { describe, expect, it } from 'vitest'; +import { + validateAndConvertV2Zarray, + validateV3Zarray, + v2ZarraySchema, + v3ZarraySchema, +} from '../src/zarrSchema.js'; + +describe('zarrextra - Zarr Schema Validation', () => { + describe('v2ZarraySchema', () => { + it('should validate a valid v2 zarray with regular dimensions', () => { + const validZarray = { + shape: [100, 200], + chunks: [10, 20], + dtype: 'float64', + zarr_format: 2, + }; + + expect(() => v2ZarraySchema.parse(validZarray)).not.toThrow(); + const result = v2ZarraySchema.parse(validZarray); + expect(result.shape).toEqual([100, 200]); + expect(result.chunks).toEqual([10, 20]); + }); + + it('should validate a scalar v2 zarray (empty shape/chunks)', () => { + const scalarZarray = { + shape: [], + chunks: [], + dtype: 'float64', + zarr_format: 2, + }; + + expect(() => v2ZarraySchema.parse(scalarZarray)).not.toThrow(); + const result = v2ZarraySchema.parse(scalarZarray); + expect(result.shape).toEqual([]); + expect(result.chunks).toEqual([]); + }); + + it('should validate v2 zarray with filters and compressor', () => { + const zarray = { + shape: [100], + chunks: [10], + dtype: 'float64', + filters: [ + { id: 'delta', configuration: { dtype: 'float64' } }, + ], + compressor: { id: 'blosc', configuration: { cname: 'lz4' } }, + }; + + expect(() => v2ZarraySchema.parse(zarray)).not.toThrow(); + const result = v2ZarraySchema.parse(zarray); + expect(result.filters).toHaveLength(1); + expect(result.compressor?.id).toBe('blosc'); + }); + + it('should reject v2 zarray with mismatched shape/chunks length', () => { + const invalidZarray = { + shape: [100, 200], + chunks: [10], // Wrong length + dtype: 'float64', + }; + + expect(() => v2ZarraySchema.parse(invalidZarray)).toThrow(); + }); + + it('should reject v2 zarray with chunk exceeding shape dimension', () => { + const invalidZarray = { + shape: [100], + chunks: [200], // Chunk larger than shape + dtype: 'float64', + }; + + expect(() => v2ZarraySchema.parse(invalidZarray)).toThrow(); + }); + + it('should reject v2 zarray with negative shape dimensions', () => { + const invalidZarray = { + shape: [-1, 100], + chunks: [10, 20], + dtype: 'float64', + }; + + expect(() => v2ZarraySchema.parse(invalidZarray)).toThrow(); + }); + + it('should reject v2 zarray with non-positive chunks', () => { + const invalidZarray = { + shape: [100], + chunks: [0], // Must be positive + dtype: 'float64', + }; + + expect(() => v2ZarraySchema.parse(invalidZarray)).toThrow(); + }); + + it('should reject v2 zarray with missing required fields', () => { + const invalidZarray = { + shape: [100], + // Missing chunks and dtype + }; + + expect(() => v2ZarraySchema.parse(invalidZarray)).toThrow(); + }); + + it('should validate v2 zarray with dimension_names matching shape length', () => { + const zarray = { + shape: [100, 200], + chunks: [10, 20], + dtype: 'float64', + dimension_names: ['y', 'x'], + }; + + expect(() => v2ZarraySchema.parse(zarray)).not.toThrow(); + const result = v2ZarraySchema.parse(zarray); + expect(result.dimension_names).toEqual(['y', 'x']); + }); + + it('should reject v2 zarray with dimension_names length mismatch', () => { + const invalidZarray = { + shape: [100, 200], + chunks: [10, 20], + dtype: 'float64', + dimension_names: ['y'], // Wrong length + }; + + expect(() => v2ZarraySchema.parse(invalidZarray)).toThrow(); + }); + }); + + describe('v3ZarraySchema', () => { + it('should validate a valid v3 zarray with regular dimensions', () => { + const validZarray = { + shape: [100, 200], + data_type: 'float64', + chunk_grid: { + name: 'regular', + configuration: { chunk_shape: [10, 20] }, + }, + chunk_key_encoding: { + name: 'default', + configuration: { separator: '/' }, + }, + node_type: 'array' as const, + }; + + expect(() => v3ZarraySchema.parse(validZarray)).not.toThrow(); + const result = v3ZarraySchema.parse(validZarray); + expect(result.shape).toEqual([100, 200]); + expect(result.chunk_grid.configuration.chunk_shape).toEqual([10, 20]); + }); + + it('should validate a scalar v3 zarray (empty shape/chunk_shape)', () => { + const scalarZarray = { + shape: [], + data_type: 'float64', + chunk_grid: { + name: 'regular', + configuration: { chunk_shape: [] }, + }, + chunk_key_encoding: { + name: 'default', + configuration: { separator: '/' }, + }, + node_type: 'array' as const, + }; + + expect(() => v3ZarraySchema.parse(scalarZarray)).not.toThrow(); + const result = v3ZarraySchema.parse(scalarZarray); + expect(result.shape).toEqual([]); + expect(result.chunk_grid.configuration.chunk_shape).toEqual([]); + }); + + it('should validate v3 zarray with codecs', () => { + const zarray = { + shape: [100], + data_type: 'float64', + chunk_grid: { + name: 'regular', + configuration: { chunk_shape: [10] }, + }, + codecs: [ + { name: 'blosc', configuration: { cname: 'lz4' } }, + ], + }; + + expect(() => v3ZarraySchema.parse(zarray)).not.toThrow(); + const result = v3ZarraySchema.parse(zarray); + expect(result.codecs).toHaveLength(1); + expect(result.codecs?.[0]?.name).toBe('blosc'); + }); + + it('should reject v3 zarray with mismatched shape/chunk_shape length', () => { + const invalidZarray = { + shape: [100, 200], + data_type: 'float64', + chunk_grid: { + name: 'regular', + configuration: { chunk_shape: [10] }, // Wrong length + }, + }; + + expect(() => v3ZarraySchema.parse(invalidZarray)).toThrow(); + }); + + it('should reject v3 zarray with chunk_shape exceeding shape dimension', () => { + const invalidZarray = { + shape: [100], + data_type: 'float64', + chunk_grid: { + name: 'regular', + configuration: { chunk_shape: [200] }, // Chunk larger than shape + }, + }; + + expect(() => v3ZarraySchema.parse(invalidZarray)).toThrow(); + }); + + it('should reject v3 zarray with missing required fields', () => { + const invalidZarray = { + shape: [100], + // Missing data_type and chunk_grid + }; + + expect(() => v3ZarraySchema.parse(invalidZarray)).toThrow(); + }); + }); + + describe('validateAndConvertV2Zarray', () => { + it('should convert valid v2 zarray to v3 format', () => { + const v2Zarray = { + shape: [100, 200], + chunks: [10, 20], + dtype: 'float64', + fill_value: 0, + zarr_format: 2, + }; + + const result = validateAndConvertV2Zarray(v2Zarray, 'test/path'); + + expect(result.shape).toEqual([100, 200]); + expect(result.data_type).toBe('float64'); + expect(result.chunk_grid.configuration.chunk_shape).toEqual([10, 20]); + expect(result.chunk_grid.name).toBe('regular'); + expect(result.chunk_key_encoding.name).toBe('default'); + expect(result.zarr_format).toBe(3); + expect(result.node_type).toBe('array'); + }); + + it('should convert scalar v2 zarray to v3 format', () => { + const v2Zarray = { + shape: [], + chunks: [], + dtype: 'float64', + zarr_format: 2, + }; + + const result = validateAndConvertV2Zarray(v2Zarray, 'test/path'); + + expect(result.shape).toEqual([]); + expect(result.chunk_grid.configuration.chunk_shape).toEqual([]); + }); + + it('should convert v2 filters and compressor to v3 codecs', () => { + const v2Zarray = { + shape: [100], + chunks: [10], + dtype: 'float64', + filters: [ + { id: 'delta', configuration: { dtype: 'float64' } }, + ], + compressor: { id: 'blosc', configuration: { cname: 'lz4' } }, + }; + + const result = validateAndConvertV2Zarray(v2Zarray, 'test/path'); + + expect(result.codecs).toHaveLength(2); + expect(result.codecs[0]?.name).toBe('delta'); + expect(result.codecs[1]?.name).toBe('blosc'); + }); + + it('should handle v2 zarray with dimension_names', () => { + const v2Zarray = { + shape: [100, 200], + chunks: [10, 20], + dtype: 'float64', + dimension_names: ['y', 'x'], + }; + + const result = validateAndConvertV2Zarray(v2Zarray, 'test/path'); + + expect(result.dimension_names).toEqual(['y', 'x']); + }); + + it('should throw error for invalid v2 zarray', () => { + const invalidZarray = { + shape: [100], + chunks: [200], // Invalid: chunk > shape + dtype: 'float64', + }; + + expect(() => { + validateAndConvertV2Zarray(invalidZarray, 'test/path'); + }).toThrow(); + }); + + it('should handle v2 zarray that is already in v3 format (hybrid)', () => { + const hybridZarray = { + shape: [100], + data_type: 'float64', + chunk_grid: { + name: 'regular', + configuration: { chunk_shape: [10] }, + }, + // Missing v2 fields (chunks, dtype) + }; + + // Should use v3 validation path + const result = validateAndConvertV2Zarray(hybridZarray, 'test/path'); + expect(result.data_type).toBe('float64'); + expect(result.chunk_grid.configuration.chunk_shape).toEqual([10]); + }); + }); + + describe('validateV3Zarray', () => { + it('should validate a valid v3 zarray', () => { + const v3Zarray = { + shape: [100, 200], + data_type: 'float64', + chunk_grid: { + name: 'regular', + configuration: { chunk_shape: [10, 20] }, + }, + chunk_key_encoding: { + name: 'default', + configuration: { separator: '/' }, + }, + node_type: 'array' as const, + }; + + const result = validateV3Zarray(v3Zarray, 'test/path'); + + expect(result.shape).toEqual([100, 200]); + expect(result.data_type).toBe('float64'); + expect(result.chunk_grid.configuration.chunk_shape).toEqual([10, 20]); + expect(result.zarr_format).toBe(3); + expect(result.node_type).toBe('array'); + }); + + it('should validate a scalar v3 zarray', () => { + const scalarZarray = { + shape: [], + data_type: 'float64', + chunk_grid: { + name: 'regular', + configuration: { chunk_shape: [] }, + }, + }; + + const result = validateV3Zarray(scalarZarray, 'test/path'); + + expect(result.shape).toEqual([]); + expect(result.chunk_grid.configuration.chunk_shape).toEqual([]); + }); + + it('should provide default chunk_key_encoding if missing', () => { + const v3Zarray = { + shape: [100], + data_type: 'float64', + chunk_grid: { + name: 'regular', + configuration: { chunk_shape: [10] }, + }, + // Missing chunk_key_encoding + }; + + const result = validateV3Zarray(v3Zarray, 'test/path'); + + expect(result.chunk_key_encoding.name).toBe('default'); + expect(result.chunk_key_encoding.configuration.separator).toBe('/'); + }); + + it('should handle optional fields with defaults', () => { + const v3Zarray = { + shape: [100], + data_type: 'float64', + chunk_grid: { + name: 'regular', + configuration: { chunk_shape: [10] }, + }, + // Missing optional fields + }; + + const result = validateV3Zarray(v3Zarray, 'test/path'); + + expect(result.fill_value).toBe(0); + expect(result.codecs).toEqual([]); + expect(result.dimension_names).toEqual([]); + expect(result.storage_transformers).toEqual([]); + }); + + it('should throw error for invalid v3 zarray', () => { + const invalidZarray = { + shape: [100], + data_type: 'float64', + chunk_grid: { + name: 'regular', + configuration: { chunk_shape: [200] }, // Invalid: chunk > shape + }, + }; + + expect(() => { + validateV3Zarray(invalidZarray, 'test/path'); + }).toThrow(); + }); + + it('should preserve provided optional fields', () => { + const v3Zarray = { + shape: [100], + data_type: 'float64', + chunk_grid: { + name: 'regular', + configuration: { chunk_shape: [10] }, + }, + fill_value: 42, + codecs: [{ name: 'blosc' }], + dimension_names: ['x'], + }; + + const result = validateV3Zarray(v3Zarray, 'test/path'); + + expect(result.fill_value).toBe(42); + expect(result.codecs).toHaveLength(1); + expect(result.dimension_names).toEqual(['x']); + }); + }); +}); + diff --git a/packages/zarrextra/vite.config.ts b/packages/zarrextra/vite.config.ts index c2c9499..3a786f8 100644 --- a/packages/zarrextra/vite.config.ts +++ b/packages/zarrextra/vite.config.ts @@ -26,6 +26,7 @@ export default defineConfig({ test: { globals: true, environment: 'node', + include: ['tests/**/*.spec.ts'], coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 72131d6..8558466 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,6 +36,9 @@ catalogs: deck.gl: specifier: ~9.1.11 version: 9.1.15 + jsdom: + specifier: ^27.4.0 + version: 27.4.0 parquet-wasm: specifier: ^0.6.1 version: 0.6.1 @@ -85,7 +88,7 @@ importers: version: 4.5.4(@types/node@22.18.8)(rollup@4.52.4)(typescript@5.9.3)(vite@7.2.7(@types/node@22.18.8)(jiti@1.21.7)(terser@5.44.0)) vitest: specifier: 'catalog:' - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.8)(jiti@1.21.7)(terser@5.44.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.8)(jiti@1.21.7)(jsdom@27.4.0)(terser@5.44.0) docs: dependencies: @@ -183,7 +186,7 @@ importers: version: 4.5.4(@types/node@22.18.8)(rollup@4.52.4)(typescript@5.9.3)(vite@7.2.7(@types/node@22.18.8)(jiti@1.21.7)(terser@5.44.0)) vitest: specifier: 'catalog:' - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.8)(jiti@1.21.7)(terser@5.44.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.8)(jiti@1.21.7)(jsdom@27.4.0)(terser@5.44.0) packages/react: dependencies: @@ -215,6 +218,9 @@ importers: '@vitejs/plugin-react': specifier: 'catalog:' version: 4.7.0(vite@7.2.7(@types/node@22.18.8)(jiti@1.21.7)(terser@5.44.0)) + jsdom: + specifier: 'catalog:' + version: 27.4.0 typescript: specifier: 'catalog:' version: 5.9.3 @@ -226,7 +232,7 @@ importers: version: 4.5.4(@types/node@22.18.8)(rollup@4.52.4)(typescript@5.9.3)(vite@7.2.7(@types/node@22.18.8)(jiti@1.21.7)(terser@5.44.0)) vitest: specifier: 'catalog:' - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.8)(jiti@1.21.7)(terser@5.44.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.8)(jiti@1.21.7)(jsdom@27.4.0)(terser@5.44.0) packages/vis: dependencies: @@ -285,6 +291,9 @@ importers: '@vitejs/plugin-react': specifier: 'catalog:' version: 4.7.0(vite@7.2.7(@types/node@22.18.8)(jiti@1.21.7)(terser@5.44.0)) + jsdom: + specifier: 'catalog:' + version: 27.4.0 typescript: specifier: 'catalog:' version: 5.9.3 @@ -296,7 +305,7 @@ importers: version: 4.5.4(@types/node@22.18.8)(rollup@4.52.4)(typescript@5.9.3)(vite@7.2.7(@types/node@22.18.8)(jiti@1.21.7)(terser@5.44.0)) vitest: specifier: 'catalog:' - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.8)(jiti@1.21.7)(terser@5.44.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.8)(jiti@1.21.7)(jsdom@27.4.0)(terser@5.44.0) packages/zarrextra: dependencies: @@ -321,10 +330,13 @@ importers: version: 4.5.4(@types/node@22.18.8)(rollup@4.52.4)(typescript@5.9.3)(vite@7.2.7(@types/node@22.18.8)(jiti@1.21.7)(terser@5.44.0)) vitest: specifier: 'catalog:' - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.8)(jiti@1.21.7)(terser@5.44.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.8)(jiti@1.21.7)(jsdom@27.4.0)(terser@5.44.0) packages: + '@acemir/cssom@0.9.31': + resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==} + '@ai-sdk/gateway@1.0.33': resolution: {integrity: sha512-v9i3GPEo4t3fGcSkQkc07xM6KJN75VUv7C1Mqmmsu2xD8lQwnQfsrgAXyNuWe20yGY0eHuheSPDZhiqsGKtH1g==} engines: {node: '>=18'} @@ -442,6 +454,15 @@ packages: '@arcgis/toolkit@4.34.9': resolution: {integrity: sha512-wFST+eVnCwmg9NyICVyn9bsBnR+TlWklsGqG3L7xqSTgfXo6TuCThE7wtTb8xWxsTBkGvImqMUgpgLuwQuTQ1g==} + '@asamuzakjp/css-color@4.1.1': + resolution: {integrity: sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==} + + '@asamuzakjp/dom-selector@6.7.6': + resolution: {integrity: sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg==} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -1115,6 +1136,9 @@ packages: peerDependencies: '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-syntax-patches-for-csstree@1.0.26': + resolution: {integrity: sha512-6boXK0KkzT5u5xOgF6TKB+CLq9SOpEGmkZw0g5n9/7yg85wab3UzSxB8TxhLJ31L4SGJ6BCFRw/iftTha1CJXA==} + '@csstools/css-tokenizer@3.0.4': resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} @@ -1816,6 +1840,15 @@ packages: resolution: {integrity: sha512-iOOuRurpjFxFVw6+aXW2JpSkRBrdOpBcbdibfPOmSPqMd1aoHBtYmYXetKoH9vfrXoBiPyO2PkDnczhsu/N9IA==} hasBin: true + '@exodus/bytes@1.10.0': + resolution: {integrity: sha512-tf8YdcbirXdPnJ+Nd4UN1EXnz+IP2DI45YVEr3vvzcVTOyrApkmIB4zvOQVd3XPr7RXnfBtAx+PXImXOIU0Ajg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + '@floating-ui/core@1.7.3': resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} @@ -3075,6 +3108,10 @@ packages: resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} engines: {node: '>= 10.0.0'} + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + aggregate-error@3.1.0: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} @@ -3265,6 +3302,9 @@ packages: batch@0.6.1: resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + big.js@5.2.2: resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} @@ -3721,6 +3761,10 @@ packages: resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css-what@6.2.2: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} @@ -3764,6 +3808,10 @@ packages: resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + cssstyle@5.3.7: + resolution: {integrity: sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==} + engines: {node: '>=20'} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -3927,6 +3975,10 @@ packages: resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} engines: {node: '>=12'} + data-urls@6.0.1: + resolution: {integrity: sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==} + engines: {node: '>=20'} + de-indent@1.0.2: resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} @@ -3950,6 +4002,9 @@ packages: supports-color: optional: true + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + deck.gl@9.1.15: resolution: {integrity: sha512-iJz8FelqonAetaNxaAlQcSmbgGJx+J1Q4KGUR5wy/4FtyTrolNrva0nLpoBWkRxMfC1dCUZNX9BFwopxoapkEA==} peerDependencies: @@ -4603,6 +4658,10 @@ packages: hpack.js@2.1.6: resolution: {integrity: sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==} + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -4658,6 +4717,10 @@ packages: http-parser-js@0.5.10: resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + http-proxy-middleware@2.0.9: resolution: {integrity: sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==} engines: {node: '>=12.0.0'} @@ -4675,6 +4738,10 @@ packages: resolution: {integrity: sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==} engines: {node: '>=10.19.0'} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -4886,6 +4953,9 @@ packages: resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} engines: {node: '>=0.10.0'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -4966,6 +5036,15 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + jsdom@27.4.0: + resolution: {integrity: sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jsep@0.3.5: resolution: {integrity: sha512-AoRLBDc6JNnKjNcmonituEABS5bcfqDhQAWWXNTFrqu6nVXBpBAGfcoTGZMFlIrh9FjmE1CQyX9CTNwZrXMMDA==} engines: {node: '>= 6.0.0'} @@ -5110,6 +5189,10 @@ packages: resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + lru-cache@11.2.5: + resolution: {integrity: sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -5226,6 +5309,9 @@ packages: mdn-data@2.0.30: resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -5656,6 +5742,9 @@ packages: parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parse5@8.0.0: + resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -6471,6 +6560,10 @@ packages: sax@1.4.3: resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -6786,6 +6879,9 @@ packages: peerDependencies: react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tabbable@6.3.0: resolution: {integrity: sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==} @@ -6873,6 +6969,13 @@ packages: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} + tldts-core@7.0.19: + resolution: {integrity: sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==} + + tldts@7.0.19: + resolution: {integrity: sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==} + hasBin: true + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -6885,6 +6988,14 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} + tough-cookie@6.0.0: + resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + tree-dump@1.1.0: resolution: {integrity: sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==} engines: {node: '>=10.0'} @@ -7160,6 +7271,10 @@ packages: vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + watchpack@2.4.4: resolution: {integrity: sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==} engines: {node: '>=10.13.0'} @@ -7173,6 +7288,10 @@ packages: web-worker@1.5.0: resolution: {integrity: sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==} + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + webpack-bundle-analyzer@4.10.2: resolution: {integrity: sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==} engines: {node: '>= 10.13.0'} @@ -7239,6 +7358,18 @@ packages: wgsl_reflect@1.2.3: resolution: {integrity: sha512-BQWBIsOn411M+ffBxmA6QRLvAOVbuz3Uk4NusxnqC1H7aeQcVLhdA3k2k/EFFFtqVjhz3z7JOOZF1a9hj2tv4Q==} + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@15.1.0: + resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} + engines: {node: '>=20'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -7307,9 +7438,16 @@ packages: resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} hasBin: true + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + xml-utils@1.10.2: resolution: {integrity: sha512-RqM+2o1RYs6T8+3DzDSoTRAUfrvaejbVHcp3+thnAtDKo8LskR+HomLajEy5UjTz24rpka7AxVBRR3g2wTUkJA==} + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xmldoc@2.0.2: resolution: {integrity: sha512-UiRwoSStEXS3R+YE8OqYv3jebza8cBBAI2y8g3B15XFkn3SbEOyyLnmPHjLBPZANrPJKEzxxB7A3XwcLikQVlQ==} engines: {node: '>=12.0.0'} @@ -7373,6 +7511,8 @@ packages: snapshots: + '@acemir/cssom@0.9.31': {} + '@ai-sdk/gateway@1.0.33(zod@4.1.13)': dependencies: '@ai-sdk/provider': 2.0.0 @@ -7562,6 +7702,24 @@ snapshots: dependencies: tslib: 2.8.1 + '@asamuzakjp/css-color@4.1.1': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 11.2.5 + + '@asamuzakjp/dom-selector@6.7.6': + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.1.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.2.5 + + '@asamuzakjp/nwsapi@2.3.9': {} + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.27.1 @@ -8404,6 +8562,8 @@ snapshots: dependencies: '@csstools/css-tokenizer': 3.0.4 + '@csstools/css-syntax-patches-for-csstree@1.0.26': {} + '@csstools/css-tokenizer@3.0.4': {} '@csstools/media-query-list-parser@4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': @@ -9713,6 +9873,8 @@ snapshots: '@esri/calcite-ui-icons@4.3.0': {} + '@exodus/bytes@1.10.0': {} + '@floating-ui/core@1.7.3': dependencies: '@floating-ui/utils': 0.2.10 @@ -11410,6 +11572,8 @@ snapshots: address@1.2.2: {} + agent-base@7.1.4: {} + aggregate-error@3.1.0: dependencies: clean-stack: 2.2.0 @@ -11615,6 +11779,10 @@ snapshots: batch@0.6.1: {} + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + big.js@5.2.2: {} bignumber.js@9.3.1: {} @@ -12090,6 +12258,11 @@ snapshots: mdn-data: 2.0.30 source-map-js: 1.2.1 + css-tree@3.1.0: + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + css-what@6.2.2: {} cssdb@8.4.2: {} @@ -12157,6 +12330,13 @@ snapshots: dependencies: css-tree: 2.2.1 + cssstyle@5.3.7: + dependencies: + '@asamuzakjp/css-color': 4.1.1 + '@csstools/css-syntax-patches-for-csstree': 1.0.26 + css-tree: 3.1.0 + lru-cache: 11.2.5 + csstype@3.2.3: {} d3-array@2.12.1: @@ -12350,6 +12530,11 @@ snapshots: d3-transition: 3.0.1(d3-selection@3.0.0) d3-zoom: 3.0.0 + data-urls@6.0.1: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 15.1.0 + de-indent@1.0.2: {} debounce@1.2.1: {} @@ -12362,6 +12547,8 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js@10.6.0: {} + deck.gl@9.1.15(@arcgis/core@4.34.5)(@luma.gl/shadertools@9.1.10(@luma.gl/core@9.1.10))(@luma.gl/webgl@9.1.10(@luma.gl/core@9.1.10))(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@deck.gl/aggregation-layers': 9.1.15(@deck.gl/core@9.1.15)(@deck.gl/layers@9.1.15(@deck.gl/core@9.1.15)(@loaders.gl/core@4.3.4)(@luma.gl/core@9.1.10)(@luma.gl/engine@9.1.10(@luma.gl/core@9.1.10)(@luma.gl/shadertools@9.1.10(@luma.gl/core@9.1.10))))(@luma.gl/core@9.1.10)(@luma.gl/engine@9.1.10(@luma.gl/core@9.1.10)(@luma.gl/shadertools@9.1.10(@luma.gl/core@9.1.10))) @@ -13146,6 +13333,12 @@ snapshots: readable-stream: 2.3.8 wbuf: 1.7.3 + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.10.0 + transitivePeerDependencies: + - '@noble/hashes' + html-escaper@2.0.2: {} html-minifier-terser@6.1.0: @@ -13217,6 +13410,13 @@ snapshots: http-parser-js@0.5.10: {} + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + http-proxy-middleware@2.0.9(@types/express@4.17.23): dependencies: '@types/http-proxy': 1.17.16 @@ -13242,6 +13442,13 @@ snapshots: quick-lru: 5.1.1 resolve-alpn: 1.2.1 + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + human-signals@2.1.0: {} hyperdyperid@1.2.0: {} @@ -13389,6 +13596,8 @@ snapshots: dependencies: isobject: 3.0.1 + is-potential-custom-element-name@1.0.1: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -13469,6 +13678,34 @@ snapshots: dependencies: argparse: 2.0.1 + jsdom@27.4.0: + dependencies: + '@acemir/cssom': 0.9.31 + '@asamuzakjp/dom-selector': 6.7.6 + '@exodus/bytes': 1.10.0 + cssstyle: 5.3.7 + data-urls: 6.0.1 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + parse5: 8.0.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + ws: 8.18.3 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + - bufferutil + - supports-color + - utf-8-validate + jsep@0.3.5: {} jsesc@3.1.0: {} @@ -13595,6 +13832,8 @@ snapshots: lowercase-keys@3.0.0: {} + lru-cache@11.2.5: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -13834,6 +14073,8 @@ snapshots: mdn-data@2.0.30: {} + mdn-data@2.12.2: {} + media-typer@0.3.0: {} memfs@4.49.0: @@ -14421,6 +14662,10 @@ snapshots: dependencies: entities: 6.0.1 + parse5@8.0.0: + dependencies: + entities: 6.0.1 + parseurl@1.3.3: {} pascal-case@3.1.2: @@ -15362,6 +15607,10 @@ snapshots: sax@1.4.3: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.27.0: {} schema-dts@1.1.5: {} @@ -15721,6 +15970,8 @@ snapshots: react: 19.2.1 use-sync-external-store: 1.6.0(react@19.2.1) + symbol-tree@3.2.4: {} + tabbable@6.3.0: {} table-layout@4.1.1: @@ -15784,6 +16035,12 @@ snapshots: tinyspy@4.0.4: {} + tldts-core@7.0.19: {} + + tldts@7.0.19: + dependencies: + tldts-core: 7.0.19 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -15792,6 +16049,14 @@ snapshots: totalist@3.0.1: {} + tough-cookie@6.0.0: + dependencies: + tldts: 7.0.19 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + tree-dump@1.1.0(tslib@2.8.1): dependencies: tslib: 2.8.1 @@ -16033,7 +16298,7 @@ snapshots: jiti: 1.21.7 terser: 5.44.0 - vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.8)(jiti@1.21.7)(terser@5.44.0): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.8)(jiti@1.21.7)(jsdom@27.4.0)(terser@5.44.0): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 @@ -16061,6 +16326,7 @@ snapshots: optionalDependencies: '@types/debug': 4.1.12 '@types/node': 22.18.8 + jsdom: 27.4.0 transitivePeerDependencies: - jiti - less @@ -16077,6 +16343,10 @@ snapshots: vscode-uri@3.1.0: {} + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + watchpack@2.4.4: dependencies: glob-to-regexp: 0.4.1 @@ -16090,6 +16360,8 @@ snapshots: web-worker@1.5.0: {} + webidl-conversions@8.0.1: {} + webpack-bundle-analyzer@4.10.2: dependencies: '@discoveryjs/json-ext': 0.5.7 @@ -16225,6 +16497,15 @@ snapshots: wgsl_reflect@1.2.3: {} + whatwg-mimetype@4.0.0: {} + + whatwg-mimetype@5.0.0: {} + + whatwg-url@15.1.0: + dependencies: + tr46: 6.0.0 + webidl-conversions: 8.0.1 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -16275,8 +16556,12 @@ snapshots: dependencies: sax: 1.4.3 + xml-name-validator@5.0.0: {} + xml-utils@1.10.2: {} + xmlchars@2.2.0: {} + xmldoc@2.0.2: dependencies: sax: 1.4.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index becb2eb..7fb9c1d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -22,3 +22,4 @@ catalog: vitest: ^3.1.4 zarrita: ^0.5.4 zod: ^4.1.13 + jsdom: ^27.4.0 diff --git a/vitest.config.ts b/vitest.config.ts index 274df48..f92a692 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -5,7 +5,11 @@ export default defineConfig({ test: { globals: true, environment: 'node', - include: ['tests/**/*.test.ts'], + include: [ + 'tests/**/*.test.ts', // Integration tests + 'packages/**/*.spec.ts', // Package unit tests (.spec.ts) + 'packages/**/*.spec.tsx', // Package unit tests for React (.spec.tsx) + ], // Integration tests may need extra time for fixture generation hooks hookTimeout: 60000, coverage: { From 7bf6b88d1ea0fe2bfdc0c1a375fbc8d3fd5c3b41 Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Fri, 30 Jan 2026 19:01:00 +0000 Subject: [PATCH 38/50] debug test ci --- .github/workflows/test.yml | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e2bbf7b..c6a0981 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,12 +39,13 @@ jobs: uses: actions/cache@v4 with: path: ~/.cache/uv - key: ${{ runner.os }}-uv-${{ hashFiles('python/v0.5.0/pyproject.toml', 'python/v0.6.1/pyproject.toml') }} + # could be overkill always running all versions in CI, may review in future. + key: ${{ runner.os }}-uv-${{ hashFiles('python/v0.5.0/pyproject.toml', 'python/v0.6.1/pyproject.toml', 'python/v0.7.0/pyproject.toml') }} # Note: Version-specific environments are set up automatically by the fixture generation script # No need to manually sync here unless you want to pre-warm the cache - - name: Generate test fixtures (Python spatialdata 0.5.0 and 0.6.1) + - name: Generate test fixtures (Python spatialdata 0.5.0, 0.6.1, and 0.7.0) run: pnpm test:fixtures:generate - name: Run unit tests @@ -53,20 +54,33 @@ jobs: - name: Run integration tests with local server shell: bash run: | + # Verify fixtures exist before starting server + if [ ! -d "test-fixtures/v0.5.0/blobs.zarr" ]; then + echo "Error: Fixtures not found at test-fixtures/v0.5.0/blobs.zarr" + echo "Listing test-fixtures directory:" + ls -la test-fixtures/ || echo "test-fixtures directory does not exist" + exit 1 + fi + # Start test server in background pnpm test:server & SERVER_PID=$! # Wait for server to be ready (up to ~30s) + # Check both root and a fixture path to ensure server is fully ready for i in {1..30}; do - if curl -sSf http://localhost:8080/ >/dev/null; then - echo "Test server is up" + if curl -sSf http://localhost:8080/ >/dev/null && \ + curl -sSf http://localhost:8080/v0.5.0/blobs.zarr/zmetadata >/dev/null; then + echo "Test server is up and serving fixtures" break fi echo "Waiting for test server on http://localhost:8080/ ..." sleep 1 done + # Small additional delay to ensure server is fully ready + sleep 2 + # Run integration tests (will hit http://localhost:8080/…) pnpm test:integration From 77d079c79c527f4fa8dc995754f7900080be1f21 Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Fri, 30 Jan 2026 19:12:51 +0000 Subject: [PATCH 39/50] segragate integration tests --- package.json | 2 +- vitest.config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 3b228b0..6edf619 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "scripts": { "build": "pnpm -r --filter='!docs' run build", "test": "pnpm -r --filter='!docs' run test", - "test:unit": "vitest run", + "test:unit": "vitest run --exclude tests/integration/**", "test:integration": "vitest run tests/integration", "test:all": "pnpm test:unit && pnpm test:integration", "test:fixtures:generate": "uv run python/scripts/generate_fixtures.py", diff --git a/vitest.config.ts b/vitest.config.ts index f92a692..f0cd13f 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -6,7 +6,7 @@ export default defineConfig({ globals: true, environment: 'node', include: [ - 'tests/**/*.test.ts', // Integration tests + 'tests/**/*.test.ts', // Integration tests (run via test:integration) 'packages/**/*.spec.ts', // Package unit tests (.spec.ts) 'packages/**/*.spec.tsx', // Package unit tests for React (.spec.tsx) ], From 234c5a3bf8a7b687f3b2906e0dca0556d3daf241 Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Fri, 30 Jan 2026 19:18:30 +0000 Subject: [PATCH 40/50] add package build step to ci --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c6a0981..d201630 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,6 +35,9 @@ jobs: - name: Install dependencies run: pnpm install + - name: Build packages + run: pnpm build + - name: Cache uv environments uses: actions/cache@v4 with: From a00e17f30cfebf37f84a8d801d9ff47a04f98260 Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Fri, 30 Jan 2026 19:20:50 +0000 Subject: [PATCH 41/50] cache pnmp install in ci --- .github/workflows/test.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d201630..5200901 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,22 +18,23 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: '22' - - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 10 run_install: false + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'pnpm' + - uses: astral-sh/setup-uv@v4 with: version: "latest" - name: Install dependencies - run: pnpm install + run: pnpm install --frozen-lockfile - name: Build packages run: pnpm build From 4ddd629c1a61b01b96127464df152718cfc28f4c Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Fri, 30 Jan 2026 19:25:56 +0000 Subject: [PATCH 42/50] fix double-quoting string --- python/scripts/validate_datasets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/scripts/validate_datasets.py b/python/scripts/validate_datasets.py index 974e088..36911c7 100755 --- a/python/scripts/validate_datasets.py +++ b/python/scripts/validate_datasets.py @@ -101,7 +101,7 @@ def test_load(): try: print("Loading dataset...", file=sys.stderr, flush=True) # Try to read the dataset - sdata = sd.read_zarr("{json.dumps(dataset['url'])}") + sdata = sd.read_zarr({json.dumps(dataset['url'])}) # Extract basic info elements = {{}} From af1a5a46608a1da0646a7c2a88e6f2e5f19f8005 Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Mon, 2 Feb 2026 11:35:43 +0000 Subject: [PATCH 43/50] make sure zarr metadata parse errors are reported as such. more precise zarr chunk separator, add v3 dtype schema. --- packages/zarrextra/src/index.ts | 23 +++++++++------- packages/zarrextra/src/zarrSchema.ts | 41 ++++++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/packages/zarrextra/src/index.ts b/packages/zarrextra/src/index.ts index 47eb8f1..60313d7 100644 --- a/packages/zarrextra/src/index.ts +++ b/packages/zarrextra/src/index.ts @@ -103,27 +103,27 @@ async function parseStoreContents(store: IntermediateConsolidatedStore): Promise return tree; } - +class ZarrSchemaError extends Error {} /** * Parse zarr v3 consolidated metadata from zarr.json * The actual zarr v3 structure has metadata nested under consolidated_metadata.metadata * We normalize it to have a top-level metadata field for internal use. * Validates all array nodes in the metadata. */ -async function parseZarrJson(zarrJson: unknown): Promise> { +async function parseZarrJson(zarrJson: unknown): Promise> { if (!zarrJson || typeof zarrJson !== 'object') { - return Err(new Error(`Invalid zarr.json: expected an object but got ${typeof zarrJson}`)); + return Err(new ZarrSchemaError(`Invalid zarr.json: expected an object but got ${typeof zarrJson}`)); } const parsed = zarrJson as ZarrV3Metadata; // Validate the structure if (!parsed.consolidated_metadata || typeof parsed.consolidated_metadata !== 'object') { - return Err(new Error('Invalid zarr.json: consolidated_metadata field is missing or not an object')); + return Err(new ZarrSchemaError('Invalid zarr.json: consolidated_metadata field is missing or not an object')); } if (!parsed.consolidated_metadata.metadata || typeof parsed.consolidated_metadata.metadata !== 'object') { - return Err(new Error('Invalid zarr.json: consolidated_metadata.metadata field is missing or not an object')); + return Err(new ZarrSchemaError('Invalid zarr.json: consolidated_metadata.metadata field is missing or not an object')); } // Validate all array nodes in the metadata @@ -136,7 +136,7 @@ async function parseZarrJson(zarrJson: unknown): Promise { + const size = Number.parseInt(val.slice(1), 10); + if (size <= 0 || size % 8 !== 0) { + ctx.addIssue({ + code: "custom", + message: `'${val}' Raw bits size must be a positive multiple of 8` + }); + } + }), +]); + /** * Base v2 zarray schema (before refines) * Note: shape and chunks can be empty arrays for scalar arrays (0-dimensional). @@ -120,7 +155,7 @@ export const v2ZarraySchema = v2ZarrayBaseSchema.refine( */ const v3ZarrayBaseSchema = z.object({ shape: z.array(nonNegativeInteger), // Empty array allowed for scalars - data_type: z.string().min(1), + data_type: v3dtypeSchema, //z.string().min(1), chunk_grid: v3ChunkGridSchema, chunk_key_encoding: v3ChunkKeyEncodingSchema.optional(), fill_value: z.union([z.number(), z.string(), z.boolean()]).optional(), @@ -247,7 +282,7 @@ export function validateV3Zarray( ): ZarrV3ArrayNode { const parseResult = v3ZarraySchema.safeParse(zarray); if (!parseResult.success) { - const errors = parseResult.error.issues.map((e: z.ZodIssue) => { + const errors = parseResult.error.issues.map((e: z.core.$ZodIssue) => { const pathStr = e.path.length > 0 ? `'${e.path.join('.')}'` : ''; return `${pathStr}${pathStr ? ': ' : ''}${e.message}`; }).join('; '); From 25d24fe7a8fd33d4305c71150b8c647ec1340a8a Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Mon, 2 Feb 2026 11:47:42 +0000 Subject: [PATCH 44/50] in generate_fixtures.py move imports into function scope and remove unused f-strings --- python/v0.5.0/generate_fixtures.py | 12 ++++++------ python/v0.6.1/generate_fixtures.py | 7 +++---- python/v0.7.0/generate_fixtures.py | 5 ++--- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/python/v0.5.0/generate_fixtures.py b/python/v0.5.0/generate_fixtures.py index 0036fe5..0346128 100755 --- a/python/v0.5.0/generate_fixtures.py +++ b/python/v0.5.0/generate_fixtures.py @@ -13,12 +13,12 @@ script_dir = Path(__file__).parent sys.path.insert(0, str(script_dir.parent / "scripts")) -from spatialdata.datasets import blobs -import spatialdata as sd def generate_fixtures(output_dir: Path): """Generate test fixtures for spatialdata version 0.5.0.""" + from spatialdata.datasets import blobs + import spatialdata as sd version = "0.5.0" print(f"Generating fixtures for spatialdata version {version}...") @@ -26,7 +26,7 @@ def generate_fixtures(output_dir: Path): actual_version = sd.__version__ if actual_version != version: print(f"⚠️ Warning: Expected version {version} but got {actual_version}") - print(f" This may indicate the wrong environment is active.") + print(" This may indicate the wrong environment is active.") # Create output directory version_dir = output_dir / f"v{version}" @@ -49,7 +49,7 @@ def generate_fixtures(output_dir: Path): # Try to delete the attribute try: delattr(sdata, "points") - except: + except: # noqa: E722 # If that fails, set to empty dict sdata.points = {} @@ -75,7 +75,7 @@ def generate_fixtures(output_dir: Path): # Force remove try: shutil.rmtree(store_path, ignore_errors=True) - except: + except: # noqa: E722 pass print(f"Saving to {store_path}...") @@ -98,7 +98,7 @@ def generate_fixtures(output_dir: Path): shutil.rmtree(store_path) else: os.remove(store_path) - except: + except: # noqa: E722 pass error_msg = str(e) diff --git a/python/v0.6.1/generate_fixtures.py b/python/v0.6.1/generate_fixtures.py index 8909b87..051418e 100755 --- a/python/v0.6.1/generate_fixtures.py +++ b/python/v0.6.1/generate_fixtures.py @@ -13,12 +13,11 @@ script_dir = Path(__file__).parent sys.path.insert(0, str(script_dir.parent / "scripts")) -from spatialdata.datasets import blobs -import spatialdata as sd - def generate_fixtures(output_dir: Path): """Generate test fixtures for spatialdata version 0.6.1.""" + from spatialdata.datasets import blobs + import spatialdata as sd version = "0.6.1" print(f"Generating fixtures for spatialdata version {version}...") @@ -26,7 +25,7 @@ def generate_fixtures(output_dir: Path): actual_version = sd.__version__ if actual_version != version: print(f"⚠️ Warning: Expected version {version} but got {actual_version}") - print(f" This may indicate the wrong environment is active.") + print(" This may indicate the wrong environment is active.") # Create output directory version_dir = output_dir / f"v{version}" diff --git a/python/v0.7.0/generate_fixtures.py b/python/v0.7.0/generate_fixtures.py index 92a3cb4..bc21313 100755 --- a/python/v0.7.0/generate_fixtures.py +++ b/python/v0.7.0/generate_fixtures.py @@ -13,12 +13,11 @@ script_dir = Path(__file__).parent sys.path.insert(0, str(script_dir.parent / "scripts")) -from spatialdata.datasets import blobs -import spatialdata as sd - def generate_fixtures(output_dir: Path): """Generate test fixtures for spatialdata version 0.7.0.""" + from spatialdata.datasets import blobs + import spatialdata as sd version = "0.7.0" print(f"Generating fixtures for spatialdata version {version}...") From 19f0eb825293a33ad5e8f55de1c66f11267dee7e Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Mon, 2 Feb 2026 11:50:57 +0000 Subject: [PATCH 45/50] clean up proxy-server on error --- scripts/validate-datasets-js.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/scripts/validate-datasets-js.js b/scripts/validate-datasets-js.js index fb3f25a..eae463a 100755 --- a/scripts/validate-datasets-js.js +++ b/scripts/validate-datasets-js.js @@ -231,6 +231,9 @@ Note: The script automatically starts and manages a CORS proxy server for all re return args; } +// Module-level variable to track proxy server for cleanup +let proxyServer = null; + /** * Main function */ @@ -256,7 +259,6 @@ async function main() { // Start proxy server automatically console.error('Starting CORS proxy server...'); - let proxyServer = null; let proxyBaseUrl = null; try { const proxy = await startProxyServer(); @@ -330,7 +332,19 @@ async function main() { }); } -main().catch(error => { +main().catch(async error => { console.error('Fatal error:', error); + + // Clean up proxy server if it was started + if (proxyServer) { + console.error('\nShutting down proxy server...'); + await new Promise((resolve) => { + proxyServer.close(() => { + console.error('✓ Proxy server stopped'); + resolve(); + }); + }); + } + process.exit(1); }); From aa2bf2ba684cddff45db29e7ac33791199c55a56 Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Mon, 2 Feb 2026 12:02:22 +0000 Subject: [PATCH 46/50] use per-segment path when retrieving attributes --- packages/zarrextra/src/index.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/zarrextra/src/index.ts b/packages/zarrextra/src/index.ts index 60313d7..cdb55e8 100644 --- a/packages/zarrextra/src/index.ts +++ b/packages/zarrextra/src/index.ts @@ -66,7 +66,8 @@ async function parseStoreContents(store: IntermediateConsolidatedStore): Promise const isArray = isLeaf && info.isArray; // Get attributes for this path (normalizedPath already has leading /) - const attrs = getZattrs(normalizedPath as zarr.AbsolutePath, store); + const currentPath = `/${pathParts.slice(0, i + 1).join('/')}` as zarr.AbsolutePath; + const attrs = getZattrs(currentPath, store); if (isArray) { // Leaf array node - extract array metadata from the node @@ -84,8 +85,8 @@ async function parseStoreContents(store: IntermediateConsolidatedStore): Promise storage_transformers: arrayNode.storage_transformers, }; - // Capture normalizedPath by value to avoid closure issues - const arrayPath = normalizedPath; + // Capture currentPath by value to avoid closure issues + const arrayPath = currentPath; currentNode[part] = { [ATTRS_KEY]: attrs, [ZARRAY_KEY]: zarray, From 9c46529b5ec5655529709e3a55d9e8344ffe370b Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Tue, 3 Feb 2026 16:03:34 +0000 Subject: [PATCH 47/50] handle root-level zattrs/zgroup --- packages/zarrextra/src/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/zarrextra/src/index.ts b/packages/zarrextra/src/index.ts index cdb55e8..ce8a82d 100644 --- a/packages/zarrextra/src/index.ts +++ b/packages/zarrextra/src/index.ts @@ -173,9 +173,10 @@ function normalizeV2ToV3Metadata(v2Metadata: ZarrV2Metadata): ZarrV3Metadata { // Iterate through flat path keys in v2 metadata for (const [flatPath, value] of Object.entries(v2Metadata.metadata)) { // Match patterns like "path/.zattrs", "path/.zarray", "path/.zgroup" - const match = flatPath.match(/^(.+?)\/(\.zattrs|\.zarray|\.zgroup)$/); + const match = flatPath.match(/^(?:(.+?)\/)?(\.zattrs|\.zarray|\.zgroup)$/); if (match) { - const [, path, metadataType] = match; + const path = match[1] ?? ''; + const metadataType = match[2]; if (!pathGroups.has(path)) { pathGroups.set(path, {}); } From 6b67da994f9d5329efe04294366ba97b31caf10b Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Tue, 3 Feb 2026 16:24:28 +0000 Subject: [PATCH 48/50] zarr schemas null support for fill_value, add storage_transformers schema, improve validation error handling for zarray metadata. Removed redundant chunk dimension checks. --- packages/zarrextra/src/zarrSchema.ts | 38 +++++++++------------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/packages/zarrextra/src/zarrSchema.ts b/packages/zarrextra/src/zarrSchema.ts index b75aa00..5dfb006 100644 --- a/packages/zarrextra/src/zarrSchema.ts +++ b/packages/zarrextra/src/zarrSchema.ts @@ -3,6 +3,7 @@ import type { ZarrV3ArrayNode } from './types'; //-------- LLM generated schemas for generic zarr metadata --------// // low - mid confidence in validity, need more testing +// (since writing the above, has been subject to at least some more human scrutiny) /** @@ -112,7 +113,7 @@ const v2ZarrayBaseSchema = z.object({ dtype: z.string().min(1), filters: z.array(v2FilterSchema).optional().nullable(), compressor: v2CompressorSchema.optional().nullable(), - fill_value: z.union([z.number(), z.string(), z.boolean()]).optional(), + fill_value: z.union([z.number(), z.string(), z.boolean(), z.null()]).optional(), dimension_names: z.array(z.string()).optional().nullable(), zarr_format: z.number().optional() }); @@ -129,17 +130,6 @@ export const v2ZarraySchema = v2ZarrayBaseSchema.refine( message: 'chunks length must match shape length', path: ['chunks'] } -).refine( - (data: V2ZarrayInput) => { - // For scalar arrays (empty shape/chunks), this check passes trivially - if (data.shape.length === 0) return true; - // For non-scalar arrays, each chunk must not exceed corresponding shape dimension - return data.chunks.every((chunk: number, i: number) => chunk <= data.shape[i]); - }, - { - message: 'each chunk dimension must not exceed corresponding shape dimension', - path: ['chunks'] - } ).refine( (data: V2ZarrayInput) => !data.dimension_names || data.dimension_names.length === data.shape.length, { @@ -147,7 +137,10 @@ export const v2ZarraySchema = v2ZarrayBaseSchema.refine( path: ['dimension_names'] } ); - +const storageTransformerSchema = z.object({ + name: z.string(), + configuration: z.unknown().optional() +}) /** * Base v3 zarray schema (before refines) * Note: shape and chunk_shape can be empty arrays for scalar arrays (0-dimensional). @@ -161,9 +154,10 @@ const v3ZarrayBaseSchema = z.object({ fill_value: z.union([z.number(), z.string(), z.boolean()]).optional(), codecs: z.array(v3CodecSchema).optional().nullable(), dimension_names: z.array(z.string()).optional().nullable(), - storage_transformers: z.array(z.unknown()).optional().nullable(), + storage_transformers: z.array(storageTransformerSchema).optional().nullable(), zarr_format: z.number().optional(), - node_type: z.literal('array').optional() + node_type: z.literal('array').optional(), + attributes: z.record(z.string(), z.json()).optional() }); type V3ZarrayInput = z.infer; @@ -178,17 +172,6 @@ export const v3ZarraySchema = v3ZarrayBaseSchema.refine( message: 'chunk_shape length must match shape length', path: ['chunk_grid', 'configuration', 'chunk_shape'] } -).refine( - (data: V3ZarrayInput) => { - // For scalar arrays (empty shape/chunk_shape), this check passes trivially - if (data.shape.length === 0) return true; - // For non-scalar arrays, each chunk must not exceed corresponding shape dimension - return data.chunk_grid.configuration.chunk_shape.every((chunk: number, i: number) => chunk <= data.shape[i]); - }, - { - message: 'each chunk dimension must not exceed corresponding shape dimension', - path: ['chunk_grid', 'configuration', 'chunk_shape'] - } ).refine( (data: V3ZarrayInput) => !data.dimension_names || data.dimension_names.length === data.shape.length, { @@ -206,6 +189,9 @@ export function validateAndConvertV2Zarray( zarray: unknown, path: string ): ZarrV3ArrayNode { + if (!zarray || typeof zarray !== 'object') { + throw new Error(`Invalid .zarray metadata at path '${path}': expected an object`); + } const obj = zarray as Record; // Check if this already has v3 fields (hybrid/partially converted format) From f6f9cc710483a4740109084a5809231e0fbf2f90 Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Tue, 3 Feb 2026 16:34:20 +0000 Subject: [PATCH 49/50] remove some badly specified tests, add tests for handling null fill_value --- packages/zarrextra/tests/zarrSchema.spec.ts | 201 ++------------------ 1 file changed, 19 insertions(+), 182 deletions(-) diff --git a/packages/zarrextra/tests/zarrSchema.spec.ts b/packages/zarrextra/tests/zarrSchema.spec.ts index 6907814..78cb4bc 100644 --- a/packages/zarrextra/tests/zarrSchema.spec.ts +++ b/packages/zarrextra/tests/zarrSchema.spec.ts @@ -8,33 +8,6 @@ import { describe('zarrextra - Zarr Schema Validation', () => { describe('v2ZarraySchema', () => { - it('should validate a valid v2 zarray with regular dimensions', () => { - const validZarray = { - shape: [100, 200], - chunks: [10, 20], - dtype: 'float64', - zarr_format: 2, - }; - - expect(() => v2ZarraySchema.parse(validZarray)).not.toThrow(); - const result = v2ZarraySchema.parse(validZarray); - expect(result.shape).toEqual([100, 200]); - expect(result.chunks).toEqual([10, 20]); - }); - - it('should validate a scalar v2 zarray (empty shape/chunks)', () => { - const scalarZarray = { - shape: [], - chunks: [], - dtype: 'float64', - zarr_format: 2, - }; - - expect(() => v2ZarraySchema.parse(scalarZarray)).not.toThrow(); - const result = v2ZarraySchema.parse(scalarZarray); - expect(result.shape).toEqual([]); - expect(result.chunks).toEqual([]); - }); it('should validate v2 zarray with filters and compressor', () => { const zarray = { @@ -63,16 +36,6 @@ describe('zarrextra - Zarr Schema Validation', () => { expect(() => v2ZarraySchema.parse(invalidZarray)).toThrow(); }); - it('should reject v2 zarray with chunk exceeding shape dimension', () => { - const invalidZarray = { - shape: [100], - chunks: [200], // Chunk larger than shape - dtype: 'float64', - }; - - expect(() => v2ZarraySchema.parse(invalidZarray)).toThrow(); - }); - it('should reject v2 zarray with negative shape dimensions', () => { const invalidZarray = { shape: [-1, 100], @@ -125,50 +88,22 @@ describe('zarrextra - Zarr Schema Validation', () => { expect(() => v2ZarraySchema.parse(invalidZarray)).toThrow(); }); - }); - describe('v3ZarraySchema', () => { - it('should validate a valid v3 zarray with regular dimensions', () => { - const validZarray = { - shape: [100, 200], - data_type: 'float64', - chunk_grid: { - name: 'regular', - configuration: { chunk_shape: [10, 20] }, - }, - chunk_key_encoding: { - name: 'default', - configuration: { separator: '/' }, - }, - node_type: 'array' as const, + it('should validate v2 zarray with null fill_value', () => { + const zarray = { + shape: [100], + chunks: [10], + dtype: 'float64', + fill_value: null, // Valid for v2 }; - expect(() => v3ZarraySchema.parse(validZarray)).not.toThrow(); - const result = v3ZarraySchema.parse(validZarray); - expect(result.shape).toEqual([100, 200]); - expect(result.chunk_grid.configuration.chunk_shape).toEqual([10, 20]); + expect(() => v2ZarraySchema.parse(zarray)).not.toThrow(); + const result = v2ZarraySchema.parse(zarray); + expect(result.fill_value).toBeNull(); }); + }); - it('should validate a scalar v3 zarray (empty shape/chunk_shape)', () => { - const scalarZarray = { - shape: [], - data_type: 'float64', - chunk_grid: { - name: 'regular', - configuration: { chunk_shape: [] }, - }, - chunk_key_encoding: { - name: 'default', - configuration: { separator: '/' }, - }, - node_type: 'array' as const, - }; - - expect(() => v3ZarraySchema.parse(scalarZarray)).not.toThrow(); - const result = v3ZarraySchema.parse(scalarZarray); - expect(result.shape).toEqual([]); - expect(result.chunk_grid.configuration.chunk_shape).toEqual([]); - }); + describe('v3ZarraySchema', () => { it('should validate v3 zarray with codecs', () => { const zarray = { @@ -202,19 +137,6 @@ describe('zarrextra - Zarr Schema Validation', () => { expect(() => v3ZarraySchema.parse(invalidZarray)).toThrow(); }); - it('should reject v3 zarray with chunk_shape exceeding shape dimension', () => { - const invalidZarray = { - shape: [100], - data_type: 'float64', - chunk_grid: { - name: 'regular', - configuration: { chunk_shape: [200] }, // Chunk larger than shape - }, - }; - - expect(() => v3ZarraySchema.parse(invalidZarray)).toThrow(); - }); - it('should reject v3 zarray with missing required fields', () => { const invalidZarray = { shape: [100], @@ -246,20 +168,6 @@ describe('zarrextra - Zarr Schema Validation', () => { expect(result.node_type).toBe('array'); }); - it('should convert scalar v2 zarray to v3 format', () => { - const v2Zarray = { - shape: [], - chunks: [], - dtype: 'float64', - zarr_format: 2, - }; - - const result = validateAndConvertV2Zarray(v2Zarray, 'test/path'); - - expect(result.shape).toEqual([]); - expect(result.chunk_grid.configuration.chunk_shape).toEqual([]); - }); - it('should convert v2 filters and compressor to v3 codecs', () => { const v2Zarray = { shape: [100], @@ -291,16 +199,18 @@ describe('zarrextra - Zarr Schema Validation', () => { expect(result.dimension_names).toEqual(['y', 'x']); }); - it('should throw error for invalid v2 zarray', () => { - const invalidZarray = { + it('should handle v2 zarray with null fill_value', () => { + const v2Zarray = { shape: [100], - chunks: [200], // Invalid: chunk > shape + chunks: [10], dtype: 'float64', + fill_value: null, // Valid for v2 }; - expect(() => { - validateAndConvertV2Zarray(invalidZarray, 'test/path'); - }).toThrow(); + const result = validateAndConvertV2Zarray(v2Zarray, 'test/path'); + // Note: validateAndConvertV2Zarray converts null fill_value to 0 default + // This is expected behavior for the conversion function + expect(result.fill_value).toBe(0); }); it('should handle v2 zarray that is already in v3 format (hybrid)', () => { @@ -322,45 +232,6 @@ describe('zarrextra - Zarr Schema Validation', () => { }); describe('validateV3Zarray', () => { - it('should validate a valid v3 zarray', () => { - const v3Zarray = { - shape: [100, 200], - data_type: 'float64', - chunk_grid: { - name: 'regular', - configuration: { chunk_shape: [10, 20] }, - }, - chunk_key_encoding: { - name: 'default', - configuration: { separator: '/' }, - }, - node_type: 'array' as const, - }; - - const result = validateV3Zarray(v3Zarray, 'test/path'); - - expect(result.shape).toEqual([100, 200]); - expect(result.data_type).toBe('float64'); - expect(result.chunk_grid.configuration.chunk_shape).toEqual([10, 20]); - expect(result.zarr_format).toBe(3); - expect(result.node_type).toBe('array'); - }); - - it('should validate a scalar v3 zarray', () => { - const scalarZarray = { - shape: [], - data_type: 'float64', - chunk_grid: { - name: 'regular', - configuration: { chunk_shape: [] }, - }, - }; - - const result = validateV3Zarray(scalarZarray, 'test/path'); - - expect(result.shape).toEqual([]); - expect(result.chunk_grid.configuration.chunk_shape).toEqual([]); - }); it('should provide default chunk_key_encoding if missing', () => { const v3Zarray = { @@ -379,40 +250,6 @@ describe('zarrextra - Zarr Schema Validation', () => { expect(result.chunk_key_encoding.configuration.separator).toBe('/'); }); - it('should handle optional fields with defaults', () => { - const v3Zarray = { - shape: [100], - data_type: 'float64', - chunk_grid: { - name: 'regular', - configuration: { chunk_shape: [10] }, - }, - // Missing optional fields - }; - - const result = validateV3Zarray(v3Zarray, 'test/path'); - - expect(result.fill_value).toBe(0); - expect(result.codecs).toEqual([]); - expect(result.dimension_names).toEqual([]); - expect(result.storage_transformers).toEqual([]); - }); - - it('should throw error for invalid v3 zarray', () => { - const invalidZarray = { - shape: [100], - data_type: 'float64', - chunk_grid: { - name: 'regular', - configuration: { chunk_shape: [200] }, // Invalid: chunk > shape - }, - }; - - expect(() => { - validateV3Zarray(invalidZarray, 'test/path'); - }).toThrow(); - }); - it('should preserve provided optional fields', () => { const v3Zarray = { shape: [100], From 2af415ef7a3012cb95fdc201aac2bdce9e1caa5d Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Tue, 3 Feb 2026 16:46:23 +0000 Subject: [PATCH 50/50] explicitly define __dirname in vitest config to avoid ambiguity --- vitest.config.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/vitest.config.ts b/vitest.config.ts index f0cd13f..d176cbe 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,5 +1,11 @@ import { defineConfig } from 'vitest/config'; -import { resolve } from 'node:path'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +// not strictly necessary as vite will provide this in config context +// but now I'm looking at it, adding this so there's less hidden magic. +const __dirname = dirname(__filename); export default defineConfig({ test: {