atlas-python 0.16.1__tar.gz → 0.16.2__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.16.1 → atlas_python-0.16.2}/Cargo.lock +2 -2
- {atlas_python-0.16.1 → atlas_python-0.16.2}/Cargo.toml +1 -1
- {atlas_python-0.16.1 → atlas_python-0.16.2}/PKG-INFO +7 -3
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/Cargo.toml +1 -1
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/README.md +6 -2
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/docs/cli.md +13 -6
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/docs/guides/creating.md +23 -6
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/docs/installation.md +41 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/tests/test_cli.py +56 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/tests/test_ops.py +35 -17
- {atlas_python-0.16.1 → atlas_python-0.16.2}/docs/python.md +9 -4
- {atlas_python-0.16.1 → atlas_python-0.16.2}/pyproject.toml +1 -1
- {atlas_python-0.16.1 → atlas_python-0.16.2}/python/atlas/__init__.pyi +12 -5
- atlas_python-0.16.2/python/atlas/__main__.py +14 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/python/atlas/_cli.py +12 -2
- {atlas_python-0.16.1 → atlas_python-0.16.2}/python/atlas/_ops.py +16 -6
- {atlas_python-0.16.1 → atlas_python-0.16.2}/.github/workflows/atlas-python-docs.yaml +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/.github/workflows/atlas-python-release.yaml +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/.github/workflows/atlas-rust-release.yaml +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/.github/workflows/ci.yaml +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/.gitignore +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/CONTRIBUTING.md +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/LICENSE +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/README.md +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/.python-version +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/LICENSE +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/docs/examples.md +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/docs/guides/cloud-storage.md +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/docs/guides/dtypes.md +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/docs/guides/inspecting.md +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/docs/guides/reading-data.md +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/docs/guides/removing.md +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/docs/index.md +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/docs/quickstart.md +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/docs/reference/api.md +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/docs/vs-zarr-netcdf.md +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/examples/01_library.py +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/examples/02_object_store.py +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/mkdocs.yml +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/src/attr.rs +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/src/dtype.rs +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/src/error.rs +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/src/lib.rs +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/src/logging.rs +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/src/reader.rs +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/src/runtime.rs +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/src/source.rs +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/src/writer.rs +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/tests/GL_PR_BO_JLKU.nc +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/tests/conftest.py +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/tests/make_fixture.py +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/tests/test_source.py +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/docs/README.md +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/docs/architecture.md +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/docs/data-model.md +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/docs/format.md +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/docs/read-path.md +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/docs/write-path.md +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/examples/lifecycle.rs +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/examples/sensor_fleet.rs +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/examples/weather_store.rs +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/python/atlas/__init__.py +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/python/atlas/_log.py +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/python/atlas/_source.py +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/python/atlas/py.typed +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/python/atlas/xarray.py +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/src/config.rs +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/src/error.rs +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/src/format/footer.rs +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/src/format/mask.rs +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/src/format/mod.rs +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/src/format/segment_store.rs +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/src/lib.rs +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/src/reader/mod.rs +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/src/schema/array.rs +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/src/schema/attr.rs +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/src/schema/dtype.rs +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/src/schema/mod.rs +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/src/writer/mod.rs +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/tests/cross_fixture.rs +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/tests/fixtures/from_python/data.atlas +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/tests/fixtures/golden_v1/data.atlas +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/tests/golden.rs +0 -0
- {atlas_python-0.16.1 → atlas_python-0.16.2}/tests/integration.rs +0 -0
|
@@ -71,7 +71,7 @@ dependencies = [
|
|
|
71
71
|
|
|
72
72
|
[[package]]
|
|
73
73
|
name = "atlas-python"
|
|
74
|
-
version = "0.16.
|
|
74
|
+
version = "0.16.2"
|
|
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.16.
|
|
89
|
+
version = "0.16.2"
|
|
90
90
|
dependencies = [
|
|
91
91
|
"array-format",
|
|
92
92
|
"async-trait",
|
|
@@ -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.16.
|
|
8
|
+
version = "0.16.2"
|
|
9
9
|
edition = "2024"
|
|
10
10
|
description = "Single-file immutable store for thousands of N-dimensional datasets, local or on object storage."
|
|
11
11
|
license = "Apache-2.0"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: atlas-python
|
|
3
|
-
Version: 0.16.
|
|
3
|
+
Version: 0.16.2
|
|
4
4
|
Classifier: Development Status :: 3 - Alpha
|
|
5
5
|
Classifier: Intended Audience :: Science/Research
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -44,6 +44,9 @@ atlas info /data/collection
|
|
|
44
44
|
atlas rm /data/collection 2024-02.nc 2024-03.nc
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
`python -m atlas` runs the same command without a PATH lookup, for a shell that
|
|
48
|
+
cannot find `atlas`.
|
|
49
|
+
|
|
47
50
|
| Extra | Install | Adds |
|
|
48
51
|
|---|---|---|
|
|
49
52
|
| cloud | `pip install "atlas-python[cloud]"` | S3 / GCS / Azure / HTTP via [obstore](https://github.com/developmentseed/obstore) |
|
|
@@ -120,8 +123,9 @@ command gives the same content as a structure.
|
|
|
120
123
|
|
|
121
124
|
## Ingest
|
|
122
125
|
|
|
123
|
-
`create` scans a directory for `.nc`, `.nc4`, `.cdf`, and `.netcdf
|
|
124
|
-
them, and writes one dataset per
|
|
126
|
+
`create` scans a directory for `.nc`, `.nc4`, `.cdf`, and `.netcdf`, and
|
|
127
|
+
descends into every subdirectory. It sorts them, and writes one dataset per
|
|
128
|
+
file, named after the file. Each coordinate
|
|
125
129
|
and data variable becomes an array. Each variable attribute becomes a per-array
|
|
126
130
|
attribute. `_FillValue` becomes the fill of the array.
|
|
127
131
|
|
|
@@ -16,6 +16,9 @@ atlas info /data/collection
|
|
|
16
16
|
atlas rm /data/collection 2024-02.nc 2024-03.nc
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
`python -m atlas` runs the same command without a PATH lookup, for a shell that
|
|
20
|
+
cannot find `atlas`.
|
|
21
|
+
|
|
19
22
|
| Extra | Install | Adds |
|
|
20
23
|
|---|---|---|
|
|
21
24
|
| cloud | `pip install "atlas-python[cloud]"` | S3 / GCS / Azure / HTTP via [obstore](https://github.com/developmentseed/obstore) |
|
|
@@ -92,8 +95,9 @@ command gives the same content as a structure.
|
|
|
92
95
|
|
|
93
96
|
## Ingest
|
|
94
97
|
|
|
95
|
-
`create` scans a directory for `.nc`, `.nc4`, `.cdf`, and `.netcdf
|
|
96
|
-
them, and writes one dataset per
|
|
98
|
+
`create` scans a directory for `.nc`, `.nc4`, `.cdf`, and `.netcdf`, and
|
|
99
|
+
descends into every subdirectory. It sorts them, and writes one dataset per
|
|
100
|
+
file, named after the file. Each coordinate
|
|
97
101
|
and data variable becomes an array. Each variable attribute becomes a per-array
|
|
98
102
|
attribute. `_FillValue` becomes the fill of the array.
|
|
99
103
|
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
`pip install atlas-python` puts `atlas` on your PATH. Five subcommands, one per
|
|
4
4
|
operation.
|
|
5
5
|
|
|
6
|
+
`python -m atlas` runs the same command without a PATH lookup. Use it in a
|
|
7
|
+
container, in a CI job, or when the shell cannot find `atlas`. See
|
|
8
|
+
[Installation](installation.md#the-atlas-command-is-not-found).
|
|
9
|
+
|
|
6
10
|
```text
|
|
7
11
|
atlas create <netcdf-dir> <collection> build a collection
|
|
8
12
|
atlas rm <collection> <name>... remove datasets
|
|
@@ -26,7 +30,7 @@ Every subcommand takes `--json` and `--log-file PATH`, plus the remote flags
|
|
|
26
30
|
```bash
|
|
27
31
|
$ atlas create /data/nc /data/collection --skip-unsupported --log-file ingest.log
|
|
28
32
|
$ cat ingest.log
|
|
29
|
-
2026-09-01 14:30:41 INFO atlas.cli: atlas 0.16.
|
|
33
|
+
2026-09-01 14:30:41 INFO atlas.cli: atlas 0.16.2: create /data/nc ...
|
|
30
34
|
2026-09-01 14:30:41 INFO atlas.ops: ingesting 2 file(s) into /data/collection
|
|
31
35
|
2026-09-01 14:30:41 WARNING atlas.ops: /data/nc/buoy.nc: skipped array 'flag' of dtype bool: numpy dtype dtype('bool') is not supported by atlas (supported: ...)
|
|
32
36
|
2026-09-01 14:30:41 INFO atlas.ops: wrote 1 dataset(s); skipped 0 file(s) and 1 array(s)
|
|
@@ -44,16 +48,19 @@ stderr.
|
|
|
44
48
|
atlas create /data/nc /data/collection
|
|
45
49
|
```
|
|
46
50
|
|
|
47
|
-
|
|
48
|
-
becomes `2024-01.nc`, suffix and
|
|
49
|
-
|
|
51
|
+
The scan descends into every subdirectory. Each NetCDF file becomes one
|
|
52
|
+
dataset, named after the file. `2024-01.nc` becomes `2024-01.nc`, suffix and
|
|
53
|
+
all. A name carries no directory, so two files of one name in two
|
|
54
|
+
subdirectories collide. The files land in sorted order, which makes the
|
|
55
|
+
ordinals of a collection reproducible.
|
|
50
56
|
|
|
51
57
|
Nothing at the destination is readable until every file lands, with the footer.
|
|
52
58
|
A failure part-way leaves no collection, and not a partial one.
|
|
53
59
|
|
|
54
60
|
| Flag | Effect |
|
|
55
61
|
|---|---|
|
|
56
|
-
|
|
|
62
|
+
| `--no-recursive` | Scan the top directory alone. The scan descends by default |
|
|
63
|
+
| `-r`, `--recursive` | Accepted for compatibility. The scan already descends |
|
|
57
64
|
| `--codec {zstd,lz4,none}` | Block compression. Default `zstd` |
|
|
58
65
|
| `--chunk-size SIZE` | Block size to aim for. Default `128MiB` |
|
|
59
66
|
| `--open-chunks MODE` | How files are read: `auto`, `native`, `none`, or a JSON dict |
|
|
@@ -121,7 +128,7 @@ reads. Each misaligned block then costs a read-modify-write. Use
|
|
|
121
128
|
|
|
122
129
|
```bash
|
|
123
130
|
# One collection from a tree of monthly directories, tolerating bad files
|
|
124
|
-
atlas create /data/nc /data/collection --
|
|
131
|
+
atlas create /data/nc /data/collection --skip-errors
|
|
125
132
|
|
|
126
133
|
# A big grid, chunked for selective reads, straight to a bucket
|
|
127
134
|
atlas create /data/nc s3://bucket/2024 --chunk-size 64MiB --region eu-west-1
|
|
@@ -15,9 +15,20 @@ atlas create /data/nc /data/collection
|
|
|
15
15
|
## What happens
|
|
16
16
|
|
|
17
17
|
`create` collects every file that matches `.nc`, `.nc4`, `.cdf`, or `.netcdf`.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
The scan **descends into every subdirectory**. It **sorts** the result, and
|
|
19
|
+
writes one dataset per file, named after the file. `2024-01.nc` becomes the
|
|
20
|
+
dataset `2024-01.nc`. The suffix is part of the name, so `jan.nc` and
|
|
21
|
+
`jan.nc4` are two datasets.
|
|
22
|
+
|
|
23
|
+
Pass `recursive=False`, or `--no-recursive`, to scan the top directory alone:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
atlas.create("/data/nc", dest, recursive=False)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
atlas create /data/nc /data/collection --no-recursive
|
|
31
|
+
```
|
|
21
32
|
|
|
22
33
|
The sort matters. An ordinal comes from the write order, so a sorted ingest
|
|
23
34
|
makes it reproducible. Rebuild the same directory, and every dataset lands at
|
|
@@ -26,8 +37,8 @@ the same position.
|
|
|
26
37
|
Check what the call picks up before you run it:
|
|
27
38
|
|
|
28
39
|
```python
|
|
29
|
-
atlas.find_netcdf_files("/data/nc")
|
|
30
|
-
atlas.find_netcdf_files("/data/nc", recursive=
|
|
40
|
+
atlas.find_netcdf_files("/data/nc") # sorted, and recursive
|
|
41
|
+
atlas.find_netcdf_files("/data/nc", recursive=False) # the top level alone
|
|
31
42
|
```
|
|
32
43
|
|
|
33
44
|
## All or nothing
|
|
@@ -236,7 +247,13 @@ turns it off.
|
|
|
236
247
|
| Destination URL cannot be resolved | `SourceError` |
|
|
237
248
|
|
|
238
249
|
Two files with one name surprise people. `a/jan.nc` and `b/jan.nc` both want
|
|
239
|
-
the name `jan.nc`.
|
|
250
|
+
the name `jan.nc`. The scan descends by default, so a tree of monthly
|
|
251
|
+
directories hits this often. A dataset name carries no directory, because a
|
|
252
|
+
name may hold no `/`.
|
|
253
|
+
|
|
254
|
+
Three ways out. Rename the files. Ingest each subdirectory into its own
|
|
255
|
+
collection. Or pass `on_error="skip"`, which keeps the first file and reports
|
|
256
|
+
the second.
|
|
240
257
|
|
|
241
258
|
For the dtype rules, see [Supported dtypes](dtypes.md).
|
|
242
259
|
|
|
@@ -47,6 +47,47 @@ To run the test suite:
|
|
|
47
47
|
pytest atlas-python/tests/ -v
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
## The `atlas` command is not found
|
|
51
|
+
|
|
52
|
+
`pip install` puts the `atlas` script in the script directory of the
|
|
53
|
+
environment it installed into. The shell finds it only when that directory is
|
|
54
|
+
on `PATH`.
|
|
55
|
+
|
|
56
|
+
Run the command through Python instead. This form needs no `PATH` entry, so it
|
|
57
|
+
works everywhere the package imports:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
python -m atlas --version
|
|
61
|
+
python -m atlas ls /data/collection
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
To fix the `PATH` itself, first find where the script landed:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip show -f atlas-python | grep -i bin
|
|
68
|
+
python -c "import sysconfig; print(sysconfig.get_path('scripts'))"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Three causes cover almost every case:
|
|
72
|
+
|
|
73
|
+
- **`pip install --user`.** The script goes to `~/.local/bin`. Many
|
|
74
|
+
distributions leave that off `PATH`. Add it:
|
|
75
|
+
`export PATH="$HOME/.local/bin:$PATH"`.
|
|
76
|
+
- **A virtual environment nobody activated.** Run `source .venv/bin/activate`,
|
|
77
|
+
or call `.venv/bin/atlas` directly.
|
|
78
|
+
- **Two Pythons.** `pip` and `python` resolve to different environments. Use
|
|
79
|
+
`python -m pip install atlas-python` to install into the interpreter you
|
|
80
|
+
actually run.
|
|
81
|
+
|
|
82
|
+
Confirm the package itself is sound:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
python -c "import atlas; print(atlas.__version__, atlas.__file__)"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
A version and a path mean the install worked, and only the script lookup
|
|
89
|
+
failed.
|
|
90
|
+
|
|
50
91
|
## Optional: cloud storage (S3, GCS, Azure)
|
|
51
92
|
|
|
52
93
|
Install the `cloud` extra to open or create an atlas store on S3, GCS, Azure
|
|
@@ -165,6 +165,27 @@ def test_log_file_records_a_failure(capsys, tmp_path):
|
|
|
165
165
|
assert "ERROR" in log.read_text()
|
|
166
166
|
|
|
167
167
|
|
|
168
|
+
def test_create_descends_by_default_and_no_recursive_opts_out(capsys, tmp_path):
|
|
169
|
+
from conftest import make_dataset
|
|
170
|
+
|
|
171
|
+
src = tmp_path / "nc"
|
|
172
|
+
(src / "sub").mkdir(parents=True)
|
|
173
|
+
make_dataset(1).to_netcdf(src / "sub" / "deep.nc")
|
|
174
|
+
make_dataset(2).to_netcdf(src / "top.nc")
|
|
175
|
+
|
|
176
|
+
run(capsys, "create", str(src), str(tmp_path / "all"), "-q")
|
|
177
|
+
code, out, _ = run(capsys, "ls", str(tmp_path / "all"))
|
|
178
|
+
assert out.split() == ["deep.nc", "top.nc"]
|
|
179
|
+
|
|
180
|
+
run(capsys, "create", str(src), str(tmp_path / "flat"), "-q", "--no-recursive")
|
|
181
|
+
code, out, _ = run(capsys, "ls", str(tmp_path / "flat"))
|
|
182
|
+
assert out.split() == ["top.nc"]
|
|
183
|
+
|
|
184
|
+
# -r still parses, so an existing script keeps working.
|
|
185
|
+
code, _, err = run(capsys, "create", str(src), str(tmp_path / "r"), "-q", "-r")
|
|
186
|
+
assert code == 0, err
|
|
187
|
+
|
|
188
|
+
|
|
168
189
|
# ── ls ───────────────────────────────────────────────────────────────
|
|
169
190
|
|
|
170
191
|
|
|
@@ -307,6 +328,41 @@ def test_info_json(capsys, collection):
|
|
|
307
328
|
assert i["format_version"] == 1
|
|
308
329
|
|
|
309
330
|
|
|
331
|
+
# ── entry points ─────────────────────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def test_python_dash_m_atlas_runs_the_cli(netcdf_dir, tmp_path):
|
|
335
|
+
"""`python -m atlas` needs no directory on PATH, so it always works."""
|
|
336
|
+
import subprocess
|
|
337
|
+
import sys
|
|
338
|
+
|
|
339
|
+
dest = tmp_path / "c"
|
|
340
|
+
done = subprocess.run(
|
|
341
|
+
[sys.executable, "-m", "atlas", "create", str(netcdf_dir), str(dest), "-q"],
|
|
342
|
+
capture_output=True,
|
|
343
|
+
text=True,
|
|
344
|
+
)
|
|
345
|
+
assert done.returncode == 0, done.stderr
|
|
346
|
+
|
|
347
|
+
done = subprocess.run(
|
|
348
|
+
[sys.executable, "-m", "atlas", "ls", str(dest)],
|
|
349
|
+
capture_output=True,
|
|
350
|
+
text=True,
|
|
351
|
+
)
|
|
352
|
+
assert done.returncode == 0, done.stderr
|
|
353
|
+
assert done.stdout.split() == ["2024-01.nc", "2024-02.nc", "2024-03.nc"]
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def test_the_console_script_is_declared():
|
|
357
|
+
"""The wheel must carry the `atlas` console script."""
|
|
358
|
+
from importlib.metadata import entry_points
|
|
359
|
+
|
|
360
|
+
scripts = {
|
|
361
|
+
e.name: e.value for e in entry_points(group="console_scripts")
|
|
362
|
+
}
|
|
363
|
+
assert scripts.get("atlas") == "atlas._cli:main"
|
|
364
|
+
|
|
365
|
+
|
|
310
366
|
# ── parsing ──────────────────────────────────────────────────────────
|
|
311
367
|
|
|
312
368
|
|
|
@@ -48,33 +48,34 @@ def test_create_is_ordered_by_filename(netcdf_dir, tmp_path):
|
|
|
48
48
|
)
|
|
49
49
|
|
|
50
50
|
|
|
51
|
-
def
|
|
51
|
+
def test_create_descends_into_subdirectories_by_default(tmp_path):
|
|
52
52
|
nested = tmp_path / "nc" / "2024" / "q1"
|
|
53
53
|
nested.mkdir(parents=True)
|
|
54
54
|
make_dataset(1).to_netcdf(nested / "jan.nc")
|
|
55
|
+
make_dataset(2).to_netcdf(tmp_path / "nc" / "top.nc")
|
|
55
56
|
|
|
56
|
-
|
|
57
|
-
assert
|
|
57
|
+
found = atlas.find_netcdf_files(tmp_path / "nc")
|
|
58
|
+
assert [p.name for p in found] == ["jan.nc", "top.nc"]
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
assert
|
|
60
|
+
atlas.create(tmp_path / "nc", str(tmp_path / "c"))
|
|
61
|
+
assert atlas.list_datasets(str(tmp_path / "c")) == ["jan.nc", "top.nc"]
|
|
61
62
|
|
|
62
|
-
atlas.create(tmp_path / "nc", str(tmp_path / "c"), recursive=True)
|
|
63
|
-
assert atlas.list_datasets(str(tmp_path / "c")) == ["jan.nc"]
|
|
64
63
|
|
|
64
|
+
def test_recursive_false_scans_the_top_directory_alone(tmp_path):
|
|
65
|
+
nested = tmp_path / "nc" / "2024" / "q1"
|
|
66
|
+
nested.mkdir(parents=True)
|
|
67
|
+
make_dataset(1).to_netcdf(nested / "jan.nc")
|
|
68
|
+
make_dataset(2).to_netcdf(tmp_path / "nc" / "top.nc")
|
|
65
69
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
with pytest.raises(atlas.AtlasError, match="no NetCDF files"):
|
|
69
|
-
atlas.create(tmp_path / "empty", str(tmp_path / "c"))
|
|
70
|
-
|
|
70
|
+
found = atlas.find_netcdf_files(tmp_path / "nc", recursive=False)
|
|
71
|
+
assert [p.name for p in found] == ["top.nc"]
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
atlas.create(tmp_path / "nope", str(tmp_path / "c"))
|
|
73
|
+
atlas.create(tmp_path / "nc", str(tmp_path / "c"), recursive=False)
|
|
74
|
+
assert atlas.list_datasets(str(tmp_path / "c")) == ["top.nc"]
|
|
75
75
|
|
|
76
76
|
|
|
77
|
-
def
|
|
77
|
+
def test_a_name_repeated_across_subdirectories_collides(tmp_path):
|
|
78
|
+
"""A dataset name carries no directory, so two `jan.nc` files clash."""
|
|
78
79
|
src = tmp_path / "nc"
|
|
79
80
|
(src / "a").mkdir(parents=True)
|
|
80
81
|
(src / "b").mkdir()
|
|
@@ -82,7 +83,24 @@ def test_two_files_with_the_same_name_collide(tmp_path):
|
|
|
82
83
|
make_dataset(2).to_netcdf(src / "b" / "jan.nc")
|
|
83
84
|
|
|
84
85
|
with pytest.raises(atlas.AtlasError, match="duplicate dataset name"):
|
|
85
|
-
atlas.create(src, str(tmp_path / "c")
|
|
86
|
+
atlas.create(src, str(tmp_path / "c"))
|
|
87
|
+
|
|
88
|
+
# on_error="skip" keeps the first and reports the second.
|
|
89
|
+
result = atlas.create(src, str(tmp_path / "c2"), on_error="skip")
|
|
90
|
+
assert result["written"] == ["jan.nc"]
|
|
91
|
+
assert len(result["skipped"]) == 1
|
|
92
|
+
assert "duplicate dataset name" in result["skipped"][0]["error"]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_an_empty_directory_is_an_error(tmp_path):
|
|
96
|
+
(tmp_path / "empty").mkdir()
|
|
97
|
+
with pytest.raises(atlas.AtlasError, match="no NetCDF files"):
|
|
98
|
+
atlas.create(tmp_path / "empty", str(tmp_path / "c"))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_a_missing_directory_is_an_error(tmp_path):
|
|
102
|
+
with pytest.raises(atlas.AtlasError, match="not a directory"):
|
|
103
|
+
atlas.create(tmp_path / "nope", str(tmp_path / "c"))
|
|
86
104
|
|
|
87
105
|
|
|
88
106
|
def test_a_bad_file_abandons_the_collection_by_default(tmp_path):
|
|
@@ -46,10 +46,15 @@ for `ATLS`, and you get nothing.
|
|
|
46
46
|
|
|
47
47
|
## Ingest
|
|
48
48
|
|
|
49
|
-
`create` scans a directory for `.nc`, `.nc4`, `.cdf`, and `.netcdf` files
|
|
50
|
-
sorts them, and writes one dataset per
|
|
51
|
-
becomes `2024-01.nc`, suffix and all.
|
|
52
|
-
Build the same directory twice, and
|
|
49
|
+
`create` scans a directory for `.nc`, `.nc4`, `.cdf`, and `.netcdf` files, and
|
|
50
|
+
descends into every subdirectory. It sorts them, and writes one dataset per
|
|
51
|
+
file, named after the file. `2024-01.nc` becomes `2024-01.nc`, suffix and all.
|
|
52
|
+
The sort makes the ordinals reproducible. Build the same directory twice, and
|
|
53
|
+
every dataset lands at the same position.
|
|
54
|
+
|
|
55
|
+
A dataset name carries no directory, because a name may hold no `/`. Two files
|
|
56
|
+
of one name in two subdirectories therefore collide. `recursive=False` scans
|
|
57
|
+
the top directory alone.
|
|
53
58
|
|
|
54
59
|
One writer does all of it. Nothing at the destination is readable until the
|
|
55
60
|
last file lands, with the footer. A failure part-way leaves no collection. That
|
|
@@ -57,7 +57,7 @@ def create(
|
|
|
57
57
|
directory: Union[str, os.PathLike[str]],
|
|
58
58
|
destination: Source,
|
|
59
59
|
*,
|
|
60
|
-
recursive: bool =
|
|
60
|
+
recursive: bool = True,
|
|
61
61
|
codec: str = "zstd",
|
|
62
62
|
chunks: Optional[dict[str, Sequence[int]]] = None,
|
|
63
63
|
open_chunks: Union[str, dict[str, int], None] = "auto",
|
|
@@ -69,10 +69,14 @@ def create(
|
|
|
69
69
|
) -> dict[str, Any]:
|
|
70
70
|
"""Builds a collection at ``destination`` from the NetCDF files in ``directory``.
|
|
71
71
|
|
|
72
|
+
The scan descends into every subdirectory. Pass ``recursive=False`` for the
|
|
73
|
+
top directory alone.
|
|
74
|
+
|
|
72
75
|
Each file becomes one dataset, named after the file. ``jan_2024.nc``
|
|
73
76
|
becomes ``jan_2024.nc``, suffix and all. The suffix tells two files apart,
|
|
74
|
-
so ``jan.nc`` and ``jan.nc4`` are two datasets.
|
|
75
|
-
|
|
77
|
+
so ``jan.nc`` and ``jan.nc4`` are two datasets. A name carries no
|
|
78
|
+
directory, so two files of one name in two subdirectories collide. The
|
|
79
|
+
files land in sorted order, which fixes the ordinals of the collection.
|
|
76
80
|
|
|
77
81
|
Nothing at ``destination`` is readable until every file lands, with the
|
|
78
82
|
footer. A failure part-way leaves no collection, not a partial one.
|
|
@@ -85,7 +89,8 @@ def create(
|
|
|
85
89
|
directory: Where the NetCDF files are.
|
|
86
90
|
destination: Where to write the collection. A local path, or a URL for
|
|
87
91
|
object storage.
|
|
88
|
-
recursive: Descend into the subdirectories.
|
|
92
|
+
recursive: Descend into the subdirectories. On by default. Set it
|
|
93
|
+
false for the top directory alone.
|
|
89
94
|
codec: Block compression. ``"zstd"`` is the default. ``"lz4"`` and
|
|
90
95
|
``"none"`` are the others. Each block records its own codec, so a
|
|
91
96
|
reader needs no argument.
|
|
@@ -225,10 +230,12 @@ def info(source: Source, **store_options: Any) -> dict[str, Any]:
|
|
|
225
230
|
...
|
|
226
231
|
|
|
227
232
|
def find_netcdf_files(
|
|
228
|
-
directory: Union[str, os.PathLike[str]], recursive: bool =
|
|
233
|
+
directory: Union[str, os.PathLike[str]], recursive: bool = True
|
|
229
234
|
) -> list[pathlib.Path]:
|
|
230
235
|
"""NetCDF files in ``directory``, sorted. :func:`create` ingests these.
|
|
231
236
|
|
|
237
|
+
The walk descends into every subdirectory, as :func:`create` does.
|
|
238
|
+
|
|
232
239
|
Call it to see what a ``create`` call picks up, before you run that call.
|
|
233
240
|
|
|
234
241
|
Raises:
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Runs the command as ``python -m atlas``.
|
|
2
|
+
|
|
3
|
+
The ``atlas`` console script needs its directory on ``PATH``. This entry point
|
|
4
|
+
does not. It works in any environment that can import the package, which makes
|
|
5
|
+
it the reliable form inside a container, a CI job, or a virtual environment
|
|
6
|
+
nobody activated.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from ._cli import main
|
|
12
|
+
|
|
13
|
+
if __name__ == "__main__":
|
|
14
|
+
sys.exit(main())
|
|
@@ -156,7 +156,7 @@ def cmd_create(args: argparse.Namespace) -> int:
|
|
|
156
156
|
result = _ops.create(
|
|
157
157
|
args.directory,
|
|
158
158
|
args.destination,
|
|
159
|
-
recursive=args.
|
|
159
|
+
recursive=not args.no_recursive,
|
|
160
160
|
codec=args.codec,
|
|
161
161
|
chunks=chunks,
|
|
162
162
|
open_chunks=open_chunks,
|
|
@@ -312,7 +312,17 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
312
312
|
)
|
|
313
313
|
p.add_argument("directory", help="directory holding the NetCDF files")
|
|
314
314
|
p.add_argument("destination", help="where to write the collection")
|
|
315
|
-
p.add_argument(
|
|
315
|
+
p.add_argument(
|
|
316
|
+
"--no-recursive",
|
|
317
|
+
action="store_true",
|
|
318
|
+
help="scan the top directory alone. The scan descends by default",
|
|
319
|
+
)
|
|
320
|
+
p.add_argument(
|
|
321
|
+
"-r",
|
|
322
|
+
"--recursive",
|
|
323
|
+
action="store_true",
|
|
324
|
+
help="accepted for compatibility. The scan already descends",
|
|
325
|
+
)
|
|
316
326
|
p.add_argument(
|
|
317
327
|
"--codec",
|
|
318
328
|
default="zstd",
|
|
@@ -56,9 +56,13 @@ def dataset_name(path: "pathlib.Path | str") -> str:
|
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
def find_netcdf_files(
|
|
59
|
-
directory: "pathlib.Path | str", recursive: bool =
|
|
59
|
+
directory: "pathlib.Path | str", recursive: bool = True
|
|
60
60
|
) -> list[pathlib.Path]:
|
|
61
|
-
"""NetCDF files in `directory`, sorted. The sort fixes a collection's order.
|
|
61
|
+
"""NetCDF files in `directory`, sorted. The sort fixes a collection's order.
|
|
62
|
+
|
|
63
|
+
The walk descends into every subdirectory. Pass `recursive=False` for the
|
|
64
|
+
top directory alone.
|
|
65
|
+
"""
|
|
62
66
|
root = pathlib.Path(directory)
|
|
63
67
|
if not root.is_dir():
|
|
64
68
|
raise AtlasError(f"not a directory: {root}")
|
|
@@ -96,7 +100,7 @@ def create(
|
|
|
96
100
|
directory: "pathlib.Path | str",
|
|
97
101
|
destination: Any,
|
|
98
102
|
*,
|
|
99
|
-
recursive: bool =
|
|
103
|
+
recursive: bool = True,
|
|
100
104
|
codec: str = "zstd",
|
|
101
105
|
chunks: Optional[dict[str, Sequence[int]]] = None,
|
|
102
106
|
open_chunks: Any = "auto",
|
|
@@ -108,10 +112,16 @@ def create(
|
|
|
108
112
|
) -> dict[str, Any]:
|
|
109
113
|
"""Builds a collection at `destination` from the NetCDF files in `directory`.
|
|
110
114
|
|
|
115
|
+
The scan descends into every subdirectory. Pass `recursive=False` for the
|
|
116
|
+
top directory alone.
|
|
117
|
+
|
|
111
118
|
Each file becomes one dataset, named after the file. `2024-01.nc` becomes
|
|
112
|
-
`2024-01.nc`, suffix and all.
|
|
113
|
-
|
|
114
|
-
|
|
119
|
+
`2024-01.nc`, suffix and all. A name carries no directory, so two files of
|
|
120
|
+
one name in two subdirectories collide. `on_error="skip"` keeps the first
|
|
121
|
+
and reports the second.
|
|
122
|
+
|
|
123
|
+
Nothing at `destination` is readable until every file lands, with the
|
|
124
|
+
footer. A failure therefore leaves no half-built collection.
|
|
115
125
|
|
|
116
126
|
Each file opens with dask chunking, under `open_chunks="auto"` by default.
|
|
117
127
|
A file far larger than memory then streams block by block. Those blocks
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|