canvod-store-metadata 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,376 @@
1
+ Metadata-Version: 2.3
2
+ Name: canvod-store-metadata
3
+ Version: 0.2.0
4
+ Summary: Rich metadata lifecycle for canvod Icechunk stores
5
+ Keywords: GNSS,VOD,metadata,icechunk,zarr,DataCite,ACDD,STAC
6
+ Author: Nicolas François Bader
7
+ Author-email: Nicolas François Bader <nicolas.bader@geo.tuwien.ac.at>
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Science/Research
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Requires-Dist: pydantic>=2.0
14
+ Requires-Dist: icechunk>=1.1.5
15
+ Requires-Dist: zarr>=3.1.2
16
+ Requires-Dist: polars>=1.0.0
17
+ Requires-Dist: canvod-utils>=0.1.0
18
+ Maintainer: Climate and Environmental Remote Sensing (CLIMERS)
19
+ Maintainer-email: Climate and Environmental Remote Sensing (CLIMERS) <nicolas.bader@geo.tuwien.ac.at>
20
+ Requires-Python: >=3.14
21
+ Description-Content-Type: text/markdown
22
+
23
+ # canvod-store-metadata
24
+
25
+ Rich metadata lifecycle for canvod Icechunk stores. Defines, collects, writes, reads,
26
+ validates, and catalogs metadata aligned with **DataCite 4.5**, **ACDD 1.3**, and
27
+ **STAC 1.1**.
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ uv add canvod-store-metadata
33
+ ```
34
+
35
+ ## Naming: "store metadata" vs "file registry"
36
+
37
+ canvod has two distinct concepts that both involve "metadata":
38
+
39
+ | Concept | Package | Location in store | What it tracks |
40
+ | --- | --- | --- | --- |
41
+ | **Store metadata** | `canvod-store-metadata` (this package) | Zarr root attrs (`canvod_metadata` key) | *Who/what/when/how* — identity, creator, provenance, environment, compliance |
42
+ | **File registry** | `canvod-store` (`MyIcechunkStore`) | `{group}/metadata/table` Zarr arrays | *Which files* went in — per-file hash, temporal range, filename, path |
43
+
44
+ In short: **store metadata** describes the store as a whole (for catalogs, reproducibility,
45
+ and compliance), while the **file registry** is an internal ingest ledger that tracks
46
+ individual source files for deduplication and auditing.
47
+
48
+ ## Quick start
49
+
50
+ ```python
51
+ from pathlib import Path
52
+ from canvod.store_metadata import (
53
+ write_metadata, read_metadata, format_metadata, validate_all,
54
+ extract_env, write_stac_collection, scan_stores,
55
+ )
56
+
57
+ # Write metadata to an existing Icechunk store
58
+ write_metadata(store_path, metadata)
59
+
60
+ # Read it back
61
+ meta = read_metadata(store_path)
62
+
63
+ # Human-readable report
64
+ print(format_metadata(meta))
65
+
66
+ # Query a single section
67
+ print(format_metadata(meta, section="env"))
68
+
69
+ # Validate against DataCite/ACDD/STAC
70
+ issues = validate_all(meta)
71
+
72
+ # Export STAC Collection JSON
73
+ write_stac_collection(store_path)
74
+
75
+ # Reproduce the exact Python environment
76
+ extract_env(store_path, Path("repro_env/"))
77
+ # then: cd repro_env && uv sync --frozen
78
+ ```
79
+
80
+ ## CLI
81
+
82
+ ```bash
83
+ python -m canvod.store_metadata.show /path/to/store # full report
84
+ python -m canvod.store_metadata.show /path/to/store identity # one section
85
+ python -m canvod.store_metadata.show /path/to/store reproduce # env reproduction
86
+ python -m canvod.store_metadata.show /path/to/store uv # raw uv.lock
87
+ python -m canvod.store_metadata.show /path/to/store toml # raw pyproject.toml
88
+ ```
89
+
90
+ ---
91
+
92
+ ## What we track (11 sections, ~90 fields)
93
+
94
+ All metadata is stored as a single JSON blob in the Zarr root attributes under the
95
+ key `canvod_metadata`. Each section is described below.
96
+
97
+ ### 1. Identity & Discovery
98
+
99
+ Identifies the store and enables discovery in catalogs.
100
+
101
+ | Field | Type | Description | Standard |
102
+ |---|---|---|---|
103
+ | `id` | str | Unique store identifier, e.g. `Rosalia/rinex_store` | DataCite, STAC |
104
+ | `title` | str | Human-readable title | DataCite, ACDD, STAC |
105
+ | `description` | str? | Free-text description | ACDD, STAC |
106
+ | `store_type` | str | `rinex_store`, `vod_store`, etc. | DataCite (resourceType) |
107
+ | `source_format` | str | `rinex3`, `rinex2`, `sbf` | — |
108
+ | `keywords` | list[str] | Discovery keywords | ACDD, STAC |
109
+ | `conventions` | str | Metadata conventions followed (default `ACDD-1.3`) | ACDD |
110
+ | `naming_authority` | str? | Naming authority URI, e.g. `at.ac.tuwien` | ACDD |
111
+ | `persistent_identifier` | str? | DOI or other persistent identifier | FAIR F1, DataCite |
112
+ | `standard_name_vocabulary` | str? | Vocabulary for standard_name attributes | ACDD |
113
+
114
+ ### 2. Creator
115
+
116
+ The person or team who produced the data.
117
+
118
+ | Field | Type | Description | Standard |
119
+ |---|---|---|---|
120
+ | `name` | str | Full name | DataCite, ACDD |
121
+ | `email` | str | Contact email | ACDD |
122
+ | `orcid` | str? | ORCID identifier | DataCite |
123
+ | `type` | str | `Personal` or `Organizational` | DataCite |
124
+ | `institution` | str | Institution name | DataCite, ACDD |
125
+ | `institution_ror` | str? | ROR identifier | DataCite |
126
+ | `department` | str? | Department or faculty | — |
127
+ | `research_group` | str? | Research group name | — |
128
+ | `website` | str? | Lab/group URL | — |
129
+
130
+ ### 3. Publisher & Rights
131
+
132
+ Licensing and publication info for repository deposits.
133
+
134
+ | Field | Type | Description | Standard |
135
+ |---|---|---|---|
136
+ | `name` | str? | Publisher name | DataCite |
137
+ | `type` | str | Publisher type (default `Institutional`) | DataCite |
138
+ | `url` | str? | Publisher URL | — |
139
+ | `license` | str? | SPDX license identifier, e.g. `CC-BY-4.0` | STAC |
140
+ | `license_uri` | str? | Full license URL | DataCite |
141
+
142
+ ### 4. Temporal Extent
143
+
144
+ When the store was created and what time period the data covers.
145
+
146
+ | Field | Type | Description | Standard |
147
+ |---|---|---|---|
148
+ | `created` | str | Store creation timestamp (ISO 8601) | DataCite |
149
+ | `updated` | str | Last update timestamp (ISO 8601) | — |
150
+ | `collected_start` | str? | Earliest data epoch | ACDD |
151
+ | `collected_end` | str? | Latest data epoch | ACDD |
152
+ | `time_coverage_start` | str? | ACDD time_coverage_start | ACDD |
153
+ | `time_coverage_end` | str? | ACDD time_coverage_end | ACDD |
154
+ | `time_coverage_duration` | str? | ISO 8601 duration, e.g. `P1D` | ACDD |
155
+ | `time_coverage_resolution` | str? | ISO 8601 duration, e.g. `PT5S` | ACDD |
156
+
157
+ ### 5. Spatial Extent & Site
158
+
159
+ Geographic location of the GNSS site.
160
+
161
+ | Field | Type | Description | Standard |
162
+ |---|---|---|---|
163
+ | `site.name` | str | Site name, e.g. `Rosalia` | — |
164
+ | `site.description` | str? | Site description | — |
165
+ | `site.country` | str? | ISO 3166-1 alpha-2, e.g. `AT` | — |
166
+ | `geospatial_lat` | float? | WGS84 latitude (degrees) | ACDD |
167
+ | `geospatial_lon` | float? | WGS84 longitude (degrees) | ACDD |
168
+ | `geospatial_alt_m` | float? | Altitude in meters | — |
169
+ | `geospatial_lat_min/max` | float? | Latitude bounds | ACDD |
170
+ | `geospatial_lon_min/max` | float? | Longitude bounds | ACDD |
171
+ | `geospatial_vertical_crs` | str | Vertical CRS (default `EPSG:4979`) | — |
172
+ | `bbox` | list[float]? | STAC bounding box `[W, S, E, N]` | STAC |
173
+ | `extent_temporal_interval` | list? | STAC temporal interval | STAC |
174
+
175
+ ### 6. Instruments & Receivers
176
+
177
+ GNSS equipment and per-receiver ingestion stats.
178
+
179
+ | Field | Type | Description | Standard |
180
+ |---|---|---|---|
181
+ | `platform` | str | Platform type (default `ground-based GNSS`) | ACDD |
182
+ | `instruments` | list[str] | Instrument names | ACDD |
183
+ | `receivers` | dict | Per-receiver info (see below) | — |
184
+
185
+ **Per receiver:**
186
+
187
+ | Field | Type | Description |
188
+ |---|---|---|
189
+ | `type` | str | `reference` or `canopy` |
190
+ | `directory` | str | Data subdirectory |
191
+ | `reader_format` | str | Reader format (default `auto`) |
192
+ | `description` | str? | Receiver description |
193
+ | `recipe` | str? | Naming recipe used |
194
+ | `epochs` | int? | Number of epochs ingested |
195
+ | `sids` | int? | Number of satellite IDs observed |
196
+ | `variables` | list[str]? | Variable names in the store group |
197
+ | `temporal_range` | list[str]? | `[start, end]` ISO timestamps |
198
+ | `metadata` | dict? | Freeform receiver metadata |
199
+
200
+ ### 7. Software Provenance
201
+
202
+ Exact software stack used to create the store.
203
+
204
+ | Field | Type | Description | Standard |
205
+ |---|---|---|---|
206
+ | `software` | dict[str, str] | Package name → version | W3C PROV |
207
+ | `python` | str? | Python version + implementation | W3C PROV |
208
+ | `uv_version` | str? | `uv` package manager version | — |
209
+ | `level` | str | Processing level: `L1` (obs store) or `L2` (VOD store) | — |
210
+ | `lineage` | str? | Processing lineage description | W3C PROV |
211
+ | `facility` | str? | Hostname of the processing machine | W3C PROV |
212
+ | `datetime` | str? | Processing timestamp | W3C PROV |
213
+
214
+ Tracked packages: `canvodpy`, `canvod-readers`, `canvod-store`, `canvod-store-metadata`,
215
+ `canvod-utils`, `icechunk`, `zarr`, `xarray`, `dask`, `numpy`, `polars`, `pydantic`.
216
+
217
+ ### 8. Environment
218
+
219
+ Hardware and runtime environment for reproducibility.
220
+
221
+ | Field | Type | Description |
222
+ |---|---|---|
223
+ | `hostname` | str? | Machine hostname |
224
+ | `os` | str? | OS platform string (e.g. `macOS-15.4-arm64-arm-64bit`) |
225
+ | `arch` | str? | CPU architecture (e.g. `arm64`) |
226
+ | `cpu_count` | int? | Number of CPU cores |
227
+ | `memory_gb` | float? | Total system RAM in GB |
228
+ | `disk_free_gb` | float? | Free disk at store path in GB |
229
+ | `dask_workers` | int? | Dask worker count used |
230
+ | `dask_threads_per_worker` | int? | Threads per Dask worker |
231
+ | `uv_lock_hash` | str? | SHA-256 of `uv.lock` for quick comparison |
232
+ | `pyproject_toml_text` | str? | **Raw monorepo `pyproject.toml`** (full text) |
233
+ | `uv_lock_text` | str? | **Raw monorepo `uv.lock`** (full text) |
234
+
235
+ The raw `pyproject.toml` and `uv.lock` are captured from the **monorepo root** (not
236
+ the individual package). This means you can recreate the exact virtual environment
237
+ that produced the store:
238
+
239
+ ```bash
240
+ # Extract and recreate
241
+ from canvod.store_metadata.show import extract_env
242
+ extract_env(Path("/path/to/store"), Path("repro_env/"))
243
+
244
+ cd repro_env
245
+ uv sync --frozen # exact same environment
246
+ ```
247
+
248
+ ### 9. Config Snapshot
249
+
250
+ Serialized processing configuration + integrity hash.
251
+
252
+ | Field | Type | Description |
253
+ |---|---|---|
254
+ | `processing` | dict? | Processing config section |
255
+ | `preprocessing` | dict? | Preprocessing config |
256
+ | `aux_data` | dict? | Auxiliary data config |
257
+ | `compression` | dict? | Compression settings |
258
+ | `icechunk` | dict? | Icechunk store config |
259
+ | `sids` | dict? | SID filtering/selection config |
260
+ | `config_hash` | str? | SHA-256 of serialized config (for quick comparison) |
261
+
262
+ ### 10. References
263
+
264
+ Citations, funding, and related resources.
265
+
266
+ | Field | Type | Description | Standard |
267
+ |---|---|---|---|
268
+ | `software_repository` | str? | Source code URL | — |
269
+ | `documentation` | str? | Documentation URL | — |
270
+ | `access_url` | str? | URL where data is accessible (FAIR A1) | FAIR |
271
+ | `related_stores` | list[str] | IDs of related stores | DataCite |
272
+ | `publications` | list | DOI + citation pairs | DataCite |
273
+ | `funding` | list | Funder, ROR, grant number, award title | DataCite |
274
+
275
+ ### 11. Summaries
276
+
277
+ Aggregate statistics about the store contents.
278
+
279
+ | Field | Type | Description |
280
+ |---|---|---|
281
+ | `total_epochs` | int? | Total number of time epochs |
282
+ | `total_sids` | int? | Total satellite IDs across all constellations |
283
+ | `constellations` | list[str]? | GNSS constellations present (G, R, E, C, ...) |
284
+ | `variables` | list[str]? | Data variable names |
285
+ | `temporal_resolution_s` | float? | Temporal resolution in seconds |
286
+ | `file_count` | int? | Number of source files ingested |
287
+ | `store_size_mb` | float? | Store size on disk in MB |
288
+ | `history` | list[str] | Timestamped processing log entries |
289
+
290
+ ---
291
+
292
+ ## FAIR data principles
293
+
294
+ The schema is designed around the [FAIR principles](https://doi.org/10.1038/sdata.2016.18)
295
+ (Findable, Accessible, Interoperable, Reusable). The table below maps each
296
+ sub-principle to the fields and features that satisfy it:
297
+
298
+ | Principle | Requirement | How we satisfy it |
299
+ |---|---|---|
300
+ | **F1** | Globally unique persistent identifier | `identity.id` (local) + `identity.persistent_identifier` (DOI) |
301
+ | **F2** | Rich metadata | 11 sections, ~90 fields |
302
+ | **F3** | Metadata includes data identifier | `identity.id` is part of the metadata blob |
303
+ | **F4** | Registered in searchable resource | `scan_stores()`, STAC catalog export |
304
+ | **A1** | Retrievable by standard protocol | Zarr/Icechunk (local, S3, HTTP); `references.access_url` |
305
+ | **A1.1** | Protocol is open and free | Zarr v3 + Icechunk — open specifications |
306
+ | **A1.2** | Protocol allows authentication | Icechunk supports S3/GCS credentials |
307
+ | **A2** | Metadata accessible if data gone | Metadata in root attrs (coupled); STAC JSON is independent |
308
+ | **I1** | Formal knowledge representation | JSON with Pydantic v2 schema |
309
+ | **I2** | FAIR-compliant vocabularies | ACDD conventions, SPDX licenses |
310
+ | **I3** | Qualified cross-references | `references.related_stores`, `references.publications` |
311
+ | **R1.1** | Clear data usage license | `publisher.license` (SPDX identifier) |
312
+ | **R1.2** | Detailed provenance | `processing` (software), `environment` (hardware), `config` (settings) |
313
+ | **R1.3** | Domain-relevant community standards | DataCite 4.5, ACDD 1.3, STAC 1.1 |
314
+
315
+ ## Standards compliance
316
+
317
+ Metadata is validated against four standards:
318
+
319
+ | Standard | What it checks |
320
+ |---|---|
321
+ | **FAIR** | All sub-principles: persistent ID, rich metadata, license, provenance, access URL, cross-references |
322
+ | **DataCite 4.5** | Mandatory fields for TU Wien Repositum deposits: id, title, creator name/institution, publication date, resource type, publisher |
323
+ | **ACDD 1.3** | Highly recommended (title, id, conventions, creator, date) + recommended (keywords, description, geospatial) |
324
+ | **STAC 1.1** | Collection required fields: id, title, description, bbox, temporal interval, license |
325
+
326
+ ```python
327
+ results = validate_all(meta)
328
+ for standard, issues in results.items():
329
+ print(f"{standard}: {'PASS' if not issues else f'{len(issues)} issues'}")
330
+ ```
331
+
332
+ ## STAC JSON export
333
+
334
+ Each store can be exported as a standalone STAC Collection JSON file:
335
+
336
+ ```python
337
+ from canvod.store_metadata import write_stac_collection, write_stac_catalog
338
+
339
+ # Single store → collection.json
340
+ write_stac_collection(store_path) # writes store_path/collection.json
341
+ write_stac_collection(store_path, Path("out.json")) # custom path
342
+
343
+ # Scan directory → catalog.json + per-store collection.json files
344
+ write_stac_catalog(root_dir)
345
+ ```
346
+
347
+ The catalog output is a valid STAC Catalog with child links pointing to each
348
+ store's `collection.json`.
349
+
350
+ ## Store inventory
351
+
352
+ Scan a directory tree for all Icechunk stores and build a Polars DataFrame catalog:
353
+
354
+ ```python
355
+ from canvod.store_metadata import scan_stores
356
+
357
+ df = scan_stores(Path("/Volumes/ExtremePro/stores/Rosalia/"))
358
+ print(df)
359
+ # ┌─────────────────────┬───────────────┬─────────┬─────────────┐
360
+ # │ id ┆ store_type ┆ site ┆ total_epochs│
361
+ # ╞═════════════════════╪═══════════════╪═════════╪═════════════╡
362
+ # │ Rosalia/rinex_store ┆ rinex_store ┆ Rosalia ┆ 86400 │
363
+ # │ Rosalia/sbf_store ┆ sbf_store ┆ Rosalia ┆ 86400 │
364
+ # └─────────────────────┴───────────────┴─────────┴─────────────┘
365
+ ```
366
+
367
+ ## Modules
368
+
369
+ | Module | Purpose |
370
+ |---|---|
371
+ | `schema.py` | Pydantic v2 models for all 11 sections |
372
+ | `collectors.py` | Pure functions that gather runtime info |
373
+ | `io.py` | Write/read/update metadata to Icechunk root attrs |
374
+ | `validate.py` | Compliance checking (DataCite, ACDD, STAC) |
375
+ | `inventory.py` | Directory scanning, catalog building, STAC export |
376
+ | `show.py` | Human-readable formatting, env extraction, CLI |