atlas-python 0.10.0__tar.gz → 0.12.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.
- {atlas_python-0.10.0 → atlas_python-0.12.0}/Cargo.lock +4 -4
- {atlas_python-0.10.0 → atlas_python-0.12.0}/Cargo.toml +2 -2
- {atlas_python-0.10.0 → atlas_python-0.12.0}/PKG-INFO +7 -5
- {atlas_python-0.10.0 → atlas_python-0.12.0}/README.md +27 -11
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/Cargo.toml +1 -1
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/README.md +6 -4
- atlas_python-0.12.0/atlas-python/docs/guides/attributes.md +115 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/docs/guides/datasets-and-arrays.md +51 -8
- atlas_python-0.12.0/atlas-python/src/attr.rs +95 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/src/dataset.rs +53 -7
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/src/error.rs +3 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/src/store.rs +65 -7
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/tests/test_xarray.py +15 -12
- {atlas_python-0.10.0 → atlas_python-0.12.0}/examples/lifecycle.rs +4 -4
- {atlas_python-0.10.0 → atlas_python-0.12.0}/examples/sensor_fleet.rs +2 -2
- {atlas_python-0.10.0 → atlas_python-0.12.0}/examples/weather_store.rs +17 -17
- {atlas_python-0.10.0 → atlas_python-0.12.0}/pyproject.toml +1 -1
- {atlas_python-0.10.0 → atlas_python-0.12.0}/python/atlas/__init__.pyi +46 -6
- {atlas_python-0.10.0 → atlas_python-0.12.0}/python/atlas/xarray.py +17 -15
- {atlas_python-0.10.0 → atlas_python-0.12.0}/src/array.rs +29 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/src/config.rs +28 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/src/dataset.rs +429 -75
- {atlas_python-0.10.0 → atlas_python-0.12.0}/src/error.rs +28 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/src/lib.rs +25 -10
- atlas_python-0.12.0/src/meta.rs +745 -0
- atlas_python-0.12.0/src/schema.rs +436 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/src/store.rs +137 -9
- {atlas_python-0.10.0 → atlas_python-0.12.0}/tests/integration.rs +352 -20
- atlas_python-0.10.0/atlas-python/docs/guides/attributes.md +0 -103
- atlas_python-0.10.0/atlas-python/src/attr.rs +0 -60
- atlas_python-0.10.0/src/meta.rs +0 -398
- atlas_python-0.10.0/src/schema.rs +0 -164
- {atlas_python-0.10.0 → atlas_python-0.12.0}/.github/workflows/atlas-python-docs.yaml +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/.github/workflows/atlas-python-release.yaml +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/.github/workflows/atlas-rust-release.yaml +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/.github/workflows/ci.yaml +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/.gitignore +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/CONTRIBUTING.md +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/LICENSE +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/.python-version +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/LICENSE +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/benchmarks/README.md +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/benchmarks/_common.py +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/benchmarks/bench_collection.py +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/benchmarks/generate_bench_charts.py +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/docs/assets/bench_gridded.svg +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/docs/assets/bench_profile.svg +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/docs/benchmarks.md +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/docs/examples.md +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/docs/guides/bulk-reads.md +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/docs/guides/cloud-storage.md +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/docs/guides/codecs-and-meta.md +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/docs/guides/dask.md +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/docs/guides/dtypes.md +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/docs/guides/durability.md +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/docs/guides/shared-arrays.md +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/docs/guides/stats.md +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/docs/guides/xarray.md +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/docs/index.md +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/docs/installation.md +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/docs/quickstart.md +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/docs/reference/atlas.md +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/docs/reference/dataset-view.md +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/docs/reference/xarray-accessor.md +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/docs/vs-zarr-netcdf.md +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/examples/01_basics.py +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/examples/02_xarray.py +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/examples/03_dask_streaming.py +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/examples/04_meta_formats.py +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/examples/05_codecs.py +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/examples/06_stats_scan.py +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/examples/07_shared_arrays.py +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/examples/08_object_store.py +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/examples/09_missing_data.py +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/mkdocs.yml +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/src/dtype.rs +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/src/lib.rs +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/src/logging.rs +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/src/runtime.rs +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/tests/GL_PR_BO_JLKU.nc +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/atlas-python/tests/test_smoke.py +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/python/atlas/__init__.py +0 -0
- {atlas_python-0.10.0 → atlas_python-0.12.0}/python/atlas/py.typed +0 -0
|
@@ -28,9 +28,9 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
|
|
28
28
|
|
|
29
29
|
[[package]]
|
|
30
30
|
name = "array-format"
|
|
31
|
-
version = "0.
|
|
31
|
+
version = "0.11.0"
|
|
32
32
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
33
|
-
checksum = "
|
|
33
|
+
checksum = "f894cd59132aa6a00c360fef55469100c39da19c556bbf44a46bd647f7252fae"
|
|
34
34
|
dependencies = [
|
|
35
35
|
"bytes",
|
|
36
36
|
"futures",
|
|
@@ -71,7 +71,7 @@ dependencies = [
|
|
|
71
71
|
|
|
72
72
|
[[package]]
|
|
73
73
|
name = "atlas-python"
|
|
74
|
-
version = "0.
|
|
74
|
+
version = "0.12.0"
|
|
75
75
|
dependencies = [
|
|
76
76
|
"atlas-rust",
|
|
77
77
|
"ndarray",
|
|
@@ -86,7 +86,7 @@ dependencies = [
|
|
|
86
86
|
|
|
87
87
|
[[package]]
|
|
88
88
|
name = "atlas-rust"
|
|
89
|
-
version = "0.
|
|
89
|
+
version = "0.12.0"
|
|
90
90
|
dependencies = [
|
|
91
91
|
"array-format",
|
|
92
92
|
"chrono",
|
|
@@ -5,7 +5,7 @@ members = ["atlas-python"]
|
|
|
5
5
|
# Registry name on crates.io (`atlas` is taken). The library is still
|
|
6
6
|
# imported as `atlas` — see `[lib]` below.
|
|
7
7
|
name = "atlas-rust"
|
|
8
|
-
version = "0.
|
|
8
|
+
version = "0.12.0"
|
|
9
9
|
edition = "2024"
|
|
10
10
|
description = "Directory-based store for thousands of N-dimensional datasets local or remote using object storage."
|
|
11
11
|
license = "Apache-2.0"
|
|
@@ -22,7 +22,7 @@ all-features = true
|
|
|
22
22
|
rustdoc-args = ["--cfg", "docsrs"]
|
|
23
23
|
|
|
24
24
|
[dependencies]
|
|
25
|
-
array-format = "0.
|
|
25
|
+
array-format = "0.11.0"
|
|
26
26
|
object_store = "0.13"
|
|
27
27
|
serde = { version = "1", features = ["derive"] }
|
|
28
28
|
serde_json = "1"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: atlas-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.0
|
|
4
4
|
Classifier: Development Status :: 3 - Alpha
|
|
5
5
|
Classifier: Intended Audience :: Science/Research
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -179,8 +179,8 @@ picklable, so call `.compute()` before handing off to distributed/multiprocessin
|
|
|
179
179
|
| Item | How it's stored |
|
|
180
180
|
| --- | --- |
|
|
181
181
|
| Each coord / data variable | A separate array, with `dims` mapped 1:1. |
|
|
182
|
-
| Dataset attrs | Dataset attributes, plain keys. |
|
|
183
|
-
| Per-variable attrs |
|
|
182
|
+
| Dataset attrs | Dataset-level (global) attributes, plain keys. |
|
|
183
|
+
| Per-variable attrs | Real per-variable attributes on each variable's array. |
|
|
184
184
|
| Per-variable `_FillValue` | Consumed by `define_array` as a typed fill value (source `Dataset.attrs` is not mutated). See [Missing data](#missing-data). |
|
|
185
185
|
| Coord vs data_var distinction | JSON list in the internal `_pyatlas_coords` attr. |
|
|
186
186
|
| Non-scalar attr values (list, ndarray) | JSON-encoded string with a `json:` prefix marker. |
|
|
@@ -261,8 +261,10 @@ handle instead of a local path. The path-based local-filesystem API works withou
|
|
|
261
261
|
| `delete_array(name)` | Tombstone the array within this dataset. |
|
|
262
262
|
| `array_meta(name) -> dict \| None` | `{"dtype", "shape", "chunk_shape", "dimension_names"}`. |
|
|
263
263
|
| `array_stats(name) -> dict \| None` | `{"row_count", "null_count", "min", "max"}` — populated after `flush()`. |
|
|
264
|
-
| `set_attribute(key, value, dtype=None)` | Type inferred from the Python value; pass `dtype` to override (e.g. `"int8"`, `"float32"`, `"timestamp_nanoseconds"`).
|
|
265
|
-
| `get_attribute(key)` / `attributes()` | Single attribute or dict of all. |
|
|
264
|
+
| `set_attribute(key, value, dtype=None)` | Set a dataset-level (global) attribute. Type inferred from the Python value; pass `dtype` to override (e.g. `"int8"`, `"float32"`, `"timestamp_nanoseconds"`). Values are stored in the `_global` `.af` file. |
|
|
265
|
+
| `get_attribute(key)` / `attributes()` | Single global attribute or dict of all. |
|
|
266
|
+
| `set_array_attribute(array, key, value, dtype=None)` | Set a per-variable attribute on `array` (e.g. `units`); `KeyError` if the array isn't defined. |
|
|
267
|
+
| `get_array_attribute(array, key)` / `array_attributes(array)` | Single per-variable attribute or dict of all, for `array`. |
|
|
266
268
|
|
|
267
269
|
`DatasetView` does **not** expose its own `flush` / `compact` — both go through the parent `Atlas`.
|
|
268
270
|
|
|
@@ -21,26 +21,30 @@ A directory-based store for thousands of named datasets, each holding N-dimensio
|
|
|
21
21
|
|
|
22
22
|
`atlas` is designed for workloads where you have a large collection of similarly-shaped datasets — such as one dataset per time step, sensor station, or simulation run — and you want to query a single variable (e.g. `temperature`) across all of them efficiently.
|
|
23
23
|
|
|
24
|
-
Each dataset is a named group of N-dimensional arrays with typed per-
|
|
24
|
+
Each dataset is a named group of N-dimensional arrays with typed attributes — both dataset-level (global) attributes and per-variable attributes. Datasets that share an array name (e.g. every `jan_2024`, `feb_2024`, … all have `temperature`) are stored together in the same physical file, keyed by dataset name inside the file.
|
|
25
25
|
|
|
26
26
|
```text
|
|
27
27
|
my_store/
|
|
28
|
-
├── atlas.json ← dataset
|
|
28
|
+
├── atlas.json ← interned per-dataset schema + attribute-key namespace (JSON)
|
|
29
|
+
├── _global/
|
|
30
|
+
│ └── data.af ← dataset-level (global) attribute values, one entry per dataset
|
|
29
31
|
├── temperature/
|
|
30
|
-
│ └── data.af ← one ArrayFile
|
|
32
|
+
│ └── data.af ← one ArrayFile: temperature + its per-variable attributes, per dataset
|
|
31
33
|
├── pressure/
|
|
32
34
|
│ └── data.af
|
|
33
35
|
└── time/
|
|
34
36
|
└── data.af
|
|
35
37
|
```
|
|
36
38
|
|
|
39
|
+
`atlas.json` holds only the **schema** of the collection. Attribute **values** live in the `.af` files: per-variable attributes on the variable's own file, and dataset-level attributes in the reserved `_global` file.
|
|
40
|
+
|
|
37
41
|
---
|
|
38
42
|
|
|
39
43
|
## Durability model
|
|
40
44
|
|
|
41
|
-
`atlas.json` is read **once** when the store is opened or created. Every subsequent mutation — `create_dataset`, `define_array`, `set_attribute`, `delete_array`, `delete_dataset` — only touches
|
|
45
|
+
`atlas.json` is read **once** when the store is opened or created. Every subsequent mutation — `create_dataset`, `define_array`, `set_attribute`, `set_array_attribute`, `delete_array`, `delete_dataset` — only touches in-memory state; array writes buffer inside the per-array in-memory layer and attribute writes buffer in a pending-attribute map. **Nothing reaches disk until `Atlas::flush()` (or `Atlas::close()`).** Dropping an `Atlas` without flushing abandons every pending in-memory write.
|
|
42
46
|
|
|
43
|
-
A single `Atlas::flush()` walks every cached `ArrayFile` (writing deltas + stats) and then serialises the in-memory
|
|
47
|
+
A single `Atlas::flush()` drains the buffered attributes into their `.af` files, walks every cached `ArrayFile` (writing deltas + stats), and then serialises the in-memory schema to `atlas.json`. This gives one durability boundary for the whole store: N datasets ⇒ one delta file per touched array name (not one per dataset) and one `atlas.json` rewrite (not N).
|
|
44
48
|
|
|
45
49
|
`DatasetView` is a borrowed handle into the atlas's shared meta — it has no `flush()` of its own.
|
|
46
50
|
|
|
@@ -50,14 +54,25 @@ A single `Atlas::flush()` walks every cached `ArrayFile` (writing deltas + stats
|
|
|
50
54
|
|
|
51
55
|
### `atlas.json`
|
|
52
56
|
|
|
53
|
-
The
|
|
57
|
+
The schema is a plain JSON file written on `Atlas::flush()` / `Atlas::close()`. It stores:
|
|
58
|
+
|
|
59
|
+
- **Store version** — for format upgrades. The current version is `2`; stores written by an older atlas are rejected on open (re-export to upgrade).
|
|
60
|
+
- **Interned schema pool** — each *distinct* per-dataset schema is stored once and referenced by index. A dataset's schema is its array schemas (per array: dtype, shape, chunk shape, named dimensions, codec) plus its **attribute-key namespace** (the names of its global and per-variable attribute keys). A collection of thousands of homogeneous datasets stores its schema a single time.
|
|
61
|
+
- **Dataset registry** — a map of dataset name → schema-pool index.
|
|
62
|
+
- **Merged schema** — a collection-wide summary listing every unique array (with its dtype widened across all datasets, its dimensions, and its per-variable attribute types) and every global attribute type. Descriptive only — reads always use each dataset's own schema — but handy for tools that want one view of "what's in here." Also exposed programmatically via `Atlas::merged_schema()`.
|
|
63
|
+
|
|
64
|
+
When the same array name (or attribute key) appears in more than one dataset with different types, the merged type is **widened**. Widening is allowed only within numeric types (e.g. `int16` ∪ `int32` → `int32`; `int32` ∪ `float32` → `float64`) or between `string` and `timestamp` (→ `string`).
|
|
65
|
+
|
|
66
|
+
Any other combination — e.g. an `int32` array in one dataset and a `string` array under the same name in another — can't merge. The dataset is **still stored** under its own type (each dataset keeps the type it declared, and its data reads back normally); the merged schema simply keeps the **first-seen** type. Whether that's reported as a warning or an error is set by `StoreConfig::on_type_mismatch`:
|
|
67
|
+
|
|
68
|
+
| `TypeMismatchPolicy` | Behaviour |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| `Warn` (default) | Logs a `tracing` warning and carries on — an ingest of many heterogeneous files won't abort because one disagrees. |
|
|
71
|
+
| `Error` | `define_array` / `set_attribute` / `set_array_attribute` return `Error::TypeMismatch`. |
|
|
54
72
|
|
|
55
|
-
|
|
56
|
-
- **Dataset names** — the complete list of datasets in the store.
|
|
57
|
-
- **Per-dataset attributes** — typed key-value pairs serialized as plain JSON values: bool, 64-bit integer, 64-bit float, UTF-8 string, or an RFC 3339 nanosecond-precision timestamp string (e.g. `"2023-11-15T07:33:20.123456789Z"`). Atlas's `Attr` enum has five variants (`Bool`/`Int64`/`Float64`/`String`/`TimestampNanoseconds`); there are no narrow integer/float types on disk.
|
|
58
|
-
- **Array schemas** — per array: dtype, shape, chunk shape, named dimensions, and the codec used when the array was first written.
|
|
73
|
+
The policy is a **per-session** choice, not an on-disk property: pass it to `Atlas::create`, or to `Atlas::open_with_config` / `Atlas::open_path_with_config` when opening an existing collection (plain `open` / `open_path` use the default). Note that these open variants honour only `on_type_mismatch` — the codec and metadata format are always detected from disk.
|
|
59
74
|
|
|
60
|
-
Because `atlas.json` is human-readable and self-describing, you can inspect or audit the
|
|
75
|
+
Attribute **values** are **not** in `atlas.json` — only their key names are (as part of the schema). Values live in the `.af` files and are read via the `array-format` attribute API. Because `atlas.json` is human-readable and self-describing, you can still inspect or audit the collection's schema with any JSON tool without needing the library.
|
|
61
76
|
|
|
62
77
|
### `<array_name>/data.af`
|
|
63
78
|
|
|
@@ -67,6 +82,7 @@ Each array variable gets its own subdirectory with a single `data.af` binary fil
|
|
|
67
82
|
- **Chunked layout** — arrays are split into chunks of a user-specified shape, so partial reads and writes touch only the relevant blocks.
|
|
68
83
|
- **Configurable compression** — each block is compressed with the codec set when the store was created (default: Zstd; also LZ4 and uncompressed). The codec is persisted in `atlas.json` and restored automatically on `open` — no need to pass it again. Block target size is 8 MiB.
|
|
69
84
|
- **Per-array fill value** — `define_array` accepts an optional `FillValue` (one of `Bool`/`Int`/`UInt`/`Float`/`String`). Unwritten cells read back as the fill value, and any *written* cell equal to it is counted as a null in the persisted stats (see below). Fill values are stored in the per-array footer; `atlas.json` is not extended.
|
|
85
|
+
- **Attribute values** — per-variable attributes (e.g. `units`) are stored in the footer of the variable's own `data.af`, keyed by dataset. Dataset-level (global) attributes live the same way in the reserved `_global/data.af`. Atlas's `Attr` type mirrors `array-format`'s `AttributeValue` (bool, sized ints/uints, `f32`/`f64`, string, binary, and typed lists) plus a nanosecond timestamp variant stored as an RFC 3339 string.
|
|
70
86
|
- **Persisted statistics** — on `Atlas::flush()`, min, max, null count, and row count are computed per array per dataset and stored alongside the data. Cells equal to the array's fill value are tallied in `null_count` and excluded from `min`/`max` (NaN fills match NaN cells by bit pattern). Statistics survive store reopening.
|
|
71
87
|
- **In-memory caches** — a 256 MiB decoded block cache and a 64 MiB raw I/O cache sit in front of the object store for repeated reads.
|
|
72
88
|
|
|
@@ -147,8 +147,8 @@ picklable, so call `.compute()` before handing off to distributed/multiprocessin
|
|
|
147
147
|
| Item | How it's stored |
|
|
148
148
|
| --- | --- |
|
|
149
149
|
| Each coord / data variable | A separate array, with `dims` mapped 1:1. |
|
|
150
|
-
| Dataset attrs | Dataset attributes, plain keys. |
|
|
151
|
-
| Per-variable attrs |
|
|
150
|
+
| Dataset attrs | Dataset-level (global) attributes, plain keys. |
|
|
151
|
+
| Per-variable attrs | Real per-variable attributes on each variable's array. |
|
|
152
152
|
| Per-variable `_FillValue` | Consumed by `define_array` as a typed fill value (source `Dataset.attrs` is not mutated). See [Missing data](#missing-data). |
|
|
153
153
|
| Coord vs data_var distinction | JSON list in the internal `_pyatlas_coords` attr. |
|
|
154
154
|
| Non-scalar attr values (list, ndarray) | JSON-encoded string with a `json:` prefix marker. |
|
|
@@ -229,8 +229,10 @@ handle instead of a local path. The path-based local-filesystem API works withou
|
|
|
229
229
|
| `delete_array(name)` | Tombstone the array within this dataset. |
|
|
230
230
|
| `array_meta(name) -> dict \| None` | `{"dtype", "shape", "chunk_shape", "dimension_names"}`. |
|
|
231
231
|
| `array_stats(name) -> dict \| None` | `{"row_count", "null_count", "min", "max"}` — populated after `flush()`. |
|
|
232
|
-
| `set_attribute(key, value, dtype=None)` | Type inferred from the Python value; pass `dtype` to override (e.g. `"int8"`, `"float32"`, `"timestamp_nanoseconds"`).
|
|
233
|
-
| `get_attribute(key)` / `attributes()` | Single attribute or dict of all. |
|
|
232
|
+
| `set_attribute(key, value, dtype=None)` | Set a dataset-level (global) attribute. Type inferred from the Python value; pass `dtype` to override (e.g. `"int8"`, `"float32"`, `"timestamp_nanoseconds"`). Values are stored in the `_global` `.af` file. |
|
|
233
|
+
| `get_attribute(key)` / `attributes()` | Single global attribute or dict of all. |
|
|
234
|
+
| `set_array_attribute(array, key, value, dtype=None)` | Set a per-variable attribute on `array` (e.g. `units`); `KeyError` if the array isn't defined. |
|
|
235
|
+
| `get_array_attribute(array, key)` / `array_attributes(array)` | Single per-variable attribute or dict of all, for `array`. |
|
|
234
236
|
|
|
235
237
|
`DatasetView` does **not** expose its own `flush` / `compact` — both go through the parent `Atlas`.
|
|
236
238
|
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Attributes
|
|
2
|
+
|
|
3
|
+
Each [`DatasetView`](../reference/dataset-view.md) carries typed attributes at
|
|
4
|
+
two levels: **dataset-level** (global) attributes and **per-variable**
|
|
5
|
+
attributes on individual arrays. Attribute *values* are stored in the `.af`
|
|
6
|
+
files — global values in a reserved `_global` file, per-variable values on the
|
|
7
|
+
variable's own file. Only the attribute *key names* are recorded in
|
|
8
|
+
`atlas.json` (as part of the schema), so listing which attributes exist is
|
|
9
|
+
cheap and doesn't load any array bytes.
|
|
10
|
+
|
|
11
|
+
## Dataset-level (global) attributes
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
ds = atlas.create_dataset("jan_2024")
|
|
15
|
+
|
|
16
|
+
ds.set_attribute("month", 1) # inferred int
|
|
17
|
+
ds.set_attribute("station", "KNMI") # inferred str
|
|
18
|
+
ds.set_attribute("calibrated", True) # inferred bool
|
|
19
|
+
|
|
20
|
+
ds.get_attribute("month") # -> 1
|
|
21
|
+
ds.get_attribute("missing") # -> None
|
|
22
|
+
ds.attributes() # -> {"month": 1, "station": "KNMI", "calibrated": True}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Per-variable attributes
|
|
26
|
+
|
|
27
|
+
Attributes can also attach to a specific array (e.g. `units` on `temperature`):
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
ds.define_array("temperature", dtype="float32", dims=["lat", "lon"], shape=[4, 8])
|
|
31
|
+
ds.set_array_attribute("temperature", "units", "degC")
|
|
32
|
+
ds.set_array_attribute("temperature", "valid_range", [-40.0, 60.0], dtype="f64")
|
|
33
|
+
|
|
34
|
+
ds.get_array_attribute("temperature", "units") # -> "degC"
|
|
35
|
+
ds.array_attributes("temperature") # -> {"units": "degC", "valid_range": [...]}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`set_array_attribute` raises `KeyError` if the array isn't defined in the
|
|
39
|
+
dataset.
|
|
40
|
+
|
|
41
|
+
## Durability
|
|
42
|
+
|
|
43
|
+
Attribute writes are buffered in memory and only reach disk on
|
|
44
|
+
[`atlas.flush()`](durability.md) (or leaving a `with atlas:` block), together
|
|
45
|
+
with the array data. The reserved `_global/data.af` file is created lazily —
|
|
46
|
+
only once a dataset actually sets a global attribute.
|
|
47
|
+
|
|
48
|
+
## The on-disk type system
|
|
49
|
+
|
|
50
|
+
Atlas's `Attr` type mirrors the underlying `array-format` `AttributeValue`:
|
|
51
|
+
|
|
52
|
+
| On-disk type | Python type returned on read |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `bool` | `bool` |
|
|
55
|
+
| `int8` / `int16` / `int32` / `int64` | `int` |
|
|
56
|
+
| `uint8` / `uint16` / `uint32` / `uint64` | `int` |
|
|
57
|
+
| `float32` / `float64` | `float` |
|
|
58
|
+
| `string` | `str` |
|
|
59
|
+
| `binary` | `bytes` |
|
|
60
|
+
| `timestamp_nanoseconds` | `int` (nanoseconds; stored as an RFC 3339 string) |
|
|
61
|
+
| any of the above as a list | `list` |
|
|
62
|
+
|
|
63
|
+
Type is inferred from the Python value by default — `int` → `int64`,
|
|
64
|
+
`float` → `float64`, `str` → `string`, `bytes` → `binary`, `bool` → `bool`.
|
|
65
|
+
|
|
66
|
+
## Overriding inferred types
|
|
67
|
+
|
|
68
|
+
Pass `dtype=` to narrow or force a specific type (works on both
|
|
69
|
+
`set_attribute` and `set_array_attribute`):
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
ds.set_attribute("sensor_id", 7, dtype="int8") # stored as int8, range-checked
|
|
73
|
+
ds.set_attribute("ratio", 0.5, dtype="float32") # stored as float32
|
|
74
|
+
ds.set_attribute("observed_at",
|
|
75
|
+
np.datetime64("2024-01-15T10:00", "ns").astype("int64").item(),
|
|
76
|
+
dtype="timestamp_nanoseconds")
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Unlike earlier versions, width hints now preserve the storage type: `dtype="int8"`
|
|
80
|
+
stores an 8-bit integer, not a widened `int64`. A `timestamp_nanoseconds`
|
|
81
|
+
attribute is stored as an RFC 3339 string and restored to a timestamp on read.
|
|
82
|
+
|
|
83
|
+
## Per-variable xarray attributes
|
|
84
|
+
|
|
85
|
+
When you write an `xr.Dataset` via `atlas.add_xarray_dataset(ds, name)`, each
|
|
86
|
+
variable's `attrs` are stored as **real per-variable attributes** on that
|
|
87
|
+
variable's array, and the dataset's own `attrs` become dataset-level
|
|
88
|
+
attributes:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
ds = xr.Dataset(
|
|
92
|
+
data_vars={"temperature": xr.DataArray(arr, dims=["lat", "lon"],
|
|
93
|
+
attrs={"units": "C"})},
|
|
94
|
+
attrs={"station": "KNMI"},
|
|
95
|
+
)
|
|
96
|
+
atlas.add_xarray_dataset(ds, "jan_2024")
|
|
97
|
+
|
|
98
|
+
view = atlas.open_dataset("jan_2024")
|
|
99
|
+
view.attributes() # {"station": "KNMI"}
|
|
100
|
+
view.array_attributes("temperature") # {"units": "C"}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
On read, `atlas.open_as_xarray_dataset("jan_2024")` puts each variable's attrs
|
|
104
|
+
back on the right `DataArray` and the global attrs back on the Dataset.
|
|
105
|
+
|
|
106
|
+
See [xarray integration](xarray.md) for the full storage convention.
|
|
107
|
+
|
|
108
|
+
## JSON-encoded "complex" attributes
|
|
109
|
+
|
|
110
|
+
xarray attribute values are sometimes nested dicts or other structures that
|
|
111
|
+
don't map to atlas's on-disk types. The xarray bridge JSON-encodes those and
|
|
112
|
+
prefixes the string with `json:`, decoding transparently on read. You generally
|
|
113
|
+
don't need to think about this, but it's why some values come back as strings
|
|
114
|
+
starting with `json:` if you read them through the raw attribute API. Simple
|
|
115
|
+
lists of scalars are stored natively as typed-list attributes.
|
|
@@ -4,18 +4,24 @@
|
|
|
4
4
|
|
|
5
5
|
An [`Atlas`](../reference/atlas.md) is a directory-backed handle. It owns:
|
|
6
6
|
|
|
7
|
-
1. An in-memory **`StoreMeta`** —
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
1. An in-memory **`StoreMeta`** — the collection's schema: every dataset and
|
|
8
|
+
every array schema, plus the attribute-key namespace (which global and
|
|
9
|
+
per-variable attribute keys exist). Distinct dataset schemas are interned,
|
|
10
|
+
so thousands of identical schemas cost one copy. Loaded once at
|
|
11
|
+
`open`/`create`, mutated by every schema write, persisted to `atlas.json`
|
|
12
|
+
on `flush()`.
|
|
10
13
|
2. A set of **array file caches** — one buffer per array name across the
|
|
11
|
-
whole store. Pending writes accumulate here until `flush()`.
|
|
14
|
+
whole store. Pending array writes accumulate here until `flush()`.
|
|
15
|
+
3. A **pending-attribute buffer** — attribute *values* set via `set_attribute`
|
|
16
|
+
/ `set_array_attribute`, drained into the `.af` files on `flush()`.
|
|
12
17
|
|
|
13
18
|
A [`DatasetView`](../reference/dataset-view.md) is a typed handle into a
|
|
14
19
|
single logical dataset. It exposes the per-dataset array schemas, the
|
|
15
|
-
per-array statistics,
|
|
16
|
-
the view (`define_array`, `write_array`,
|
|
17
|
-
parent atlas's in-memory
|
|
18
|
-
*
|
|
20
|
+
per-array statistics, dataset-level (global) attributes, and per-variable
|
|
21
|
+
attributes. Mutations through the view (`define_array`, `write_array`,
|
|
22
|
+
`set_attribute`, `set_array_attribute`, …) update the parent atlas's in-memory
|
|
23
|
+
state; attribute *values* live in the `.af` files, and nothing reaches disk
|
|
24
|
+
until you flush the *atlas*.
|
|
19
25
|
|
|
20
26
|
```text
|
|
21
27
|
Atlas ── StoreMeta (in-memory) ─┬─ DatasetView "jan_2024"
|
|
@@ -142,3 +148,40 @@ jan.delete_array("temperature") # tombstone within this dataset
|
|
|
142
148
|
|
|
143
149
|
The array's bytes inside the shared physical file are tombstoned; reclaim
|
|
144
150
|
the space with [`atlas.compact()`](durability.md).
|
|
151
|
+
|
|
152
|
+
## Merged schema and type widening
|
|
153
|
+
|
|
154
|
+
`atlas.json` also carries a collection-wide **merged schema** — every unique
|
|
155
|
+
array (with its dtype, dimensions, and per-variable attribute types) and every
|
|
156
|
+
global attribute type, folded across all datasets. Read it with
|
|
157
|
+
`atlas.merged_schema()`:
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
merged = store.merged_schema()
|
|
161
|
+
merged["arrays"]["temperature"]["dtype"] # widened dtype across datasets
|
|
162
|
+
merged["arrays"]["temperature"]["attributes"] # {attr_key: dtype}
|
|
163
|
+
merged["global_attributes"] # {attr_key: dtype}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
When the same array name or attribute key appears in multiple datasets with
|
|
167
|
+
different types, the merged type is **widened** — but only within numeric types
|
|
168
|
+
(`int16` ∪ `int32` → `int32`, `int32` ∪ `float32` → `float64`) or between
|
|
169
|
+
`string` and `timestamp` (→ `string`).
|
|
170
|
+
|
|
171
|
+
Anything else can't merge (e.g. an `int32` array in one dataset and a `string`
|
|
172
|
+
array under the same name in another). The dataset is **still stored** under its
|
|
173
|
+
own type and reads back normally; the merged schema just keeps the
|
|
174
|
+
**first-seen** type. `on_type_mismatch` decides how that's reported:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
# "warn" (default): stored, logs a warning, merged keeps the first type
|
|
178
|
+
store = atlas.Atlas.create("/tmp/store")
|
|
179
|
+
store = atlas.Atlas.open("/tmp/store")
|
|
180
|
+
|
|
181
|
+
# "error": the mismatching define_array / set_attribute raises ValueError
|
|
182
|
+
store = atlas.Atlas.create("/tmp/store", on_type_mismatch="error")
|
|
183
|
+
store = atlas.Atlas.open("/tmp/store", on_type_mismatch="error")
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
It's a per-session choice — it isn't stored in `atlas.json`, so pass it each
|
|
187
|
+
time you open the collection.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
use atlas::Attr;
|
|
2
|
+
use pyo3::exceptions::PyValueError;
|
|
3
|
+
use pyo3::prelude::*;
|
|
4
|
+
use pyo3::types::{PyBool, PyBytes, PyFloat, PyInt, PyList, PyString};
|
|
5
|
+
|
|
6
|
+
pub fn py_to_attr(value: &Bound<'_, PyAny>, dtype_hint: Option<&str>) -> PyResult<Attr> {
|
|
7
|
+
if let Some(hint) = dtype_hint {
|
|
8
|
+
return py_to_attr_typed(value, hint);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if let Ok(b) = value.downcast::<PyBool>() {
|
|
12
|
+
return Ok(Attr::Bool(b.is_true()));
|
|
13
|
+
}
|
|
14
|
+
if value.downcast::<PyString>().is_ok() {
|
|
15
|
+
return Ok(Attr::String(value.extract::<String>()?));
|
|
16
|
+
}
|
|
17
|
+
if value.downcast::<PyBytes>().is_ok() {
|
|
18
|
+
return Ok(Attr::Binary(value.extract::<Vec<u8>>()?));
|
|
19
|
+
}
|
|
20
|
+
if value.downcast::<PyInt>().is_ok() {
|
|
21
|
+
return Ok(Attr::Int64(value.extract::<i64>()?));
|
|
22
|
+
}
|
|
23
|
+
if value.downcast::<PyFloat>().is_ok() {
|
|
24
|
+
return Ok(Attr::Float64(value.extract::<f64>()?));
|
|
25
|
+
}
|
|
26
|
+
Err(PyValueError::new_err(format!(
|
|
27
|
+
"unsupported attribute type: {:?}",
|
|
28
|
+
value.get_type().name()?
|
|
29
|
+
)))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fn py_to_attr_typed(value: &Bound<'_, PyAny>, dtype: &str) -> PyResult<Attr> {
|
|
33
|
+
Ok(match dtype.to_ascii_lowercase().as_str() {
|
|
34
|
+
"bool" => Attr::Bool(value.extract()?),
|
|
35
|
+
// Width-precise integer hints so the on-disk attribute keeps its type.
|
|
36
|
+
// Python `int.extract::<T>()` raises OverflowError on overflow, which
|
|
37
|
+
// surfaces as a PyErr.
|
|
38
|
+
"i8" | "int8" => Attr::Int8(value.extract()?),
|
|
39
|
+
"i16" | "int16" => Attr::Int16(value.extract()?),
|
|
40
|
+
"i32" | "int32" => Attr::Int32(value.extract()?),
|
|
41
|
+
"i64" | "int64" => Attr::Int64(value.extract()?),
|
|
42
|
+
"u8" | "uint8" => Attr::UInt8(value.extract()?),
|
|
43
|
+
"u16" | "uint16" => Attr::UInt16(value.extract()?),
|
|
44
|
+
"u32" | "uint32" => Attr::UInt32(value.extract()?),
|
|
45
|
+
"u64" | "uint64" => Attr::UInt64(value.extract()?),
|
|
46
|
+
"f32" | "float32" => Attr::Float32(value.extract()?),
|
|
47
|
+
"f64" | "float64" => Attr::Float64(value.extract()?),
|
|
48
|
+
"string" | "str" => Attr::String(value.extract()?),
|
|
49
|
+
"binary" | "bytes" => Attr::Binary(value.extract()?),
|
|
50
|
+
"timestamp_ns" | "timestamp_nanoseconds" | "datetime64[ns]" => {
|
|
51
|
+
Attr::TimestampNanoseconds(value.extract()?)
|
|
52
|
+
}
|
|
53
|
+
other => {
|
|
54
|
+
return Err(PyValueError::new_err(format!(
|
|
55
|
+
"unknown attribute dtype: {other:?}"
|
|
56
|
+
)))
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
pub fn attr_to_py(py: Python<'_>, attr: &Attr) -> PyResult<Py<PyAny>> {
|
|
62
|
+
use pyo3::IntoPyObjectExt;
|
|
63
|
+
match attr {
|
|
64
|
+
Attr::Bool(v) => (*v).into_py_any(py),
|
|
65
|
+
Attr::Int8(v) => (*v).into_py_any(py),
|
|
66
|
+
Attr::Int16(v) => (*v).into_py_any(py),
|
|
67
|
+
Attr::Int32(v) => (*v).into_py_any(py),
|
|
68
|
+
Attr::Int64(v) => (*v).into_py_any(py),
|
|
69
|
+
Attr::UInt8(v) => (*v).into_py_any(py),
|
|
70
|
+
Attr::UInt16(v) => (*v).into_py_any(py),
|
|
71
|
+
Attr::UInt32(v) => (*v).into_py_any(py),
|
|
72
|
+
Attr::UInt64(v) => (*v).into_py_any(py),
|
|
73
|
+
Attr::Float32(v) => (*v).into_py_any(py),
|
|
74
|
+
Attr::Float64(v) => (*v).into_py_any(py),
|
|
75
|
+
Attr::String(v) => v.clone().into_py_any(py),
|
|
76
|
+
Attr::Binary(v) => PyBytes::new(py, v).into_py_any(py),
|
|
77
|
+
Attr::TimestampNanoseconds(v) => (*v).into_py_any(py),
|
|
78
|
+
Attr::BoolList(v) => PyList::new(py, v)?.into_py_any(py),
|
|
79
|
+
Attr::Int8List(v) => PyList::new(py, v)?.into_py_any(py),
|
|
80
|
+
Attr::Int16List(v) => PyList::new(py, v)?.into_py_any(py),
|
|
81
|
+
Attr::Int32List(v) => PyList::new(py, v)?.into_py_any(py),
|
|
82
|
+
Attr::Int64List(v) => PyList::new(py, v)?.into_py_any(py),
|
|
83
|
+
Attr::UInt8List(v) => PyList::new(py, v)?.into_py_any(py),
|
|
84
|
+
Attr::UInt16List(v) => PyList::new(py, v)?.into_py_any(py),
|
|
85
|
+
Attr::UInt32List(v) => PyList::new(py, v)?.into_py_any(py),
|
|
86
|
+
Attr::UInt64List(v) => PyList::new(py, v)?.into_py_any(py),
|
|
87
|
+
Attr::Float32List(v) => PyList::new(py, v)?.into_py_any(py),
|
|
88
|
+
Attr::Float64List(v) => PyList::new(py, v)?.into_py_any(py),
|
|
89
|
+
Attr::StringList(v) => PyList::new(py, v)?.into_py_any(py),
|
|
90
|
+
Attr::BinaryList(v) => {
|
|
91
|
+
let items: Vec<Bound<'_, PyBytes>> = v.iter().map(|b| PyBytes::new(py, b)).collect();
|
|
92
|
+
PyList::new(py, items)?.into_py_any(py)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -71,10 +71,13 @@ impl PyDatasetView {
|
|
|
71
71
|
.collect()
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
/// Returns a dict of attribute name -> Python value.
|
|
74
|
+
/// Returns a dict of dataset-level (global) attribute name -> Python value.
|
|
75
75
|
fn attributes<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
|
|
76
|
+
let attrs = py
|
|
77
|
+
.detach(|| runtime().block_on(self.inner.attributes()))
|
|
78
|
+
.map_err(to_py_err)?;
|
|
76
79
|
let dict = PyDict::new(py);
|
|
77
|
-
for (k, v) in &
|
|
80
|
+
for (k, v) in &attrs {
|
|
78
81
|
dict.set_item(k, attr_to_py(py, v)?)?;
|
|
79
82
|
}
|
|
80
83
|
Ok(dict)
|
|
@@ -88,15 +91,58 @@ impl PyDatasetView {
|
|
|
88
91
|
dtype: Option<&str>,
|
|
89
92
|
) -> PyResult<()> {
|
|
90
93
|
let attr = py_to_attr(value, dtype)?;
|
|
91
|
-
self.inner.set_attribute(key, attr)
|
|
92
|
-
Ok(())
|
|
94
|
+
self.inner.set_attribute(key, attr).map_err(to_py_err)
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
fn get_attribute(&self, py: Python<'_>, key: &str) -> PyResult<Option<Py<PyAny>>> {
|
|
98
|
+
let attr = py
|
|
99
|
+
.detach(|| runtime().block_on(self.inner.get_attribute(key)))
|
|
100
|
+
.map_err(to_py_err)?;
|
|
101
|
+
attr.map(|attr| attr_to_py(py, &attr)).transpose()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// Set a per-variable (per-array) attribute, e.g. `units` on `temperature`.
|
|
105
|
+
#[pyo3(signature = (array, key, value, dtype=None))]
|
|
106
|
+
fn set_array_attribute(
|
|
107
|
+
&mut self,
|
|
108
|
+
array: &str,
|
|
109
|
+
key: &str,
|
|
110
|
+
value: &Bound<'_, PyAny>,
|
|
111
|
+
dtype: Option<&str>,
|
|
112
|
+
) -> PyResult<()> {
|
|
113
|
+
let attr = py_to_attr(value, dtype)?;
|
|
96
114
|
self.inner
|
|
97
|
-
.
|
|
98
|
-
.
|
|
99
|
-
|
|
115
|
+
.set_array_attribute(array, key, attr)
|
|
116
|
+
.map_err(to_py_err)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// Get a per-variable attribute on `array`, or `None` if unset.
|
|
120
|
+
fn get_array_attribute(
|
|
121
|
+
&self,
|
|
122
|
+
py: Python<'_>,
|
|
123
|
+
array: &str,
|
|
124
|
+
key: &str,
|
|
125
|
+
) -> PyResult<Option<Py<PyAny>>> {
|
|
126
|
+
let attr = py
|
|
127
|
+
.detach(|| runtime().block_on(self.inner.get_array_attribute(array, key)))
|
|
128
|
+
.map_err(to_py_err)?;
|
|
129
|
+
attr.map(|attr| attr_to_py(py, &attr)).transpose()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/// Returns a dict of per-variable attribute name -> Python value for `array`.
|
|
133
|
+
fn array_attributes<'py>(
|
|
134
|
+
&self,
|
|
135
|
+
py: Python<'py>,
|
|
136
|
+
array: &str,
|
|
137
|
+
) -> PyResult<Bound<'py, PyDict>> {
|
|
138
|
+
let attrs = py
|
|
139
|
+
.detach(|| runtime().block_on(self.inner.array_attributes(array)))
|
|
140
|
+
.map_err(to_py_err)?;
|
|
141
|
+
let dict = PyDict::new(py);
|
|
142
|
+
for (k, v) in &attrs {
|
|
143
|
+
dict.set_item(k, attr_to_py(py, v)?)?;
|
|
144
|
+
}
|
|
145
|
+
Ok(dict)
|
|
100
146
|
}
|
|
101
147
|
|
|
102
148
|
/// Returns `{"dtype", "shape", "chunk_shape", "dimension_names"}` for
|
|
@@ -21,6 +21,9 @@ pub fn to_py_err(err: atlas::Error) -> PyErr {
|
|
|
21
21
|
PyValueError::new_err(format!("invalid name: {name}"))
|
|
22
22
|
}
|
|
23
23
|
atlas::Error::StoreNotFound => PyFileNotFoundError::new_err("store not found at path"),
|
|
24
|
+
e @ (atlas::Error::UnsupportedVersion { .. } | atlas::Error::TypeMismatch { .. }) => {
|
|
25
|
+
PyValueError::new_err(e.to_string())
|
|
26
|
+
}
|
|
24
27
|
atlas::Error::Io(e) => PyOSError::new_err(e.to_string()),
|
|
25
28
|
e @ (atlas::Error::ObjectStore(_)
|
|
26
29
|
| atlas::Error::ArrayFormat(_)
|