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.
Files changed (84) hide show
  1. {atlas_python-0.16.1 → atlas_python-0.16.2}/Cargo.lock +2 -2
  2. {atlas_python-0.16.1 → atlas_python-0.16.2}/Cargo.toml +1 -1
  3. {atlas_python-0.16.1 → atlas_python-0.16.2}/PKG-INFO +7 -3
  4. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/Cargo.toml +1 -1
  5. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/README.md +6 -2
  6. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/docs/cli.md +13 -6
  7. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/docs/guides/creating.md +23 -6
  8. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/docs/installation.md +41 -0
  9. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/tests/test_cli.py +56 -0
  10. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/tests/test_ops.py +35 -17
  11. {atlas_python-0.16.1 → atlas_python-0.16.2}/docs/python.md +9 -4
  12. {atlas_python-0.16.1 → atlas_python-0.16.2}/pyproject.toml +1 -1
  13. {atlas_python-0.16.1 → atlas_python-0.16.2}/python/atlas/__init__.pyi +12 -5
  14. atlas_python-0.16.2/python/atlas/__main__.py +14 -0
  15. {atlas_python-0.16.1 → atlas_python-0.16.2}/python/atlas/_cli.py +12 -2
  16. {atlas_python-0.16.1 → atlas_python-0.16.2}/python/atlas/_ops.py +16 -6
  17. {atlas_python-0.16.1 → atlas_python-0.16.2}/.github/workflows/atlas-python-docs.yaml +0 -0
  18. {atlas_python-0.16.1 → atlas_python-0.16.2}/.github/workflows/atlas-python-release.yaml +0 -0
  19. {atlas_python-0.16.1 → atlas_python-0.16.2}/.github/workflows/atlas-rust-release.yaml +0 -0
  20. {atlas_python-0.16.1 → atlas_python-0.16.2}/.github/workflows/ci.yaml +0 -0
  21. {atlas_python-0.16.1 → atlas_python-0.16.2}/.gitignore +0 -0
  22. {atlas_python-0.16.1 → atlas_python-0.16.2}/CONTRIBUTING.md +0 -0
  23. {atlas_python-0.16.1 → atlas_python-0.16.2}/LICENSE +0 -0
  24. {atlas_python-0.16.1 → atlas_python-0.16.2}/README.md +0 -0
  25. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/.python-version +0 -0
  26. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/LICENSE +0 -0
  27. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/docs/examples.md +0 -0
  28. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/docs/guides/cloud-storage.md +0 -0
  29. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/docs/guides/dtypes.md +0 -0
  30. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/docs/guides/inspecting.md +0 -0
  31. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/docs/guides/reading-data.md +0 -0
  32. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/docs/guides/removing.md +0 -0
  33. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/docs/index.md +0 -0
  34. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/docs/quickstart.md +0 -0
  35. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/docs/reference/api.md +0 -0
  36. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/docs/vs-zarr-netcdf.md +0 -0
  37. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/examples/01_library.py +0 -0
  38. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/examples/02_object_store.py +0 -0
  39. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/mkdocs.yml +0 -0
  40. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/src/attr.rs +0 -0
  41. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/src/dtype.rs +0 -0
  42. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/src/error.rs +0 -0
  43. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/src/lib.rs +0 -0
  44. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/src/logging.rs +0 -0
  45. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/src/reader.rs +0 -0
  46. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/src/runtime.rs +0 -0
  47. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/src/source.rs +0 -0
  48. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/src/writer.rs +0 -0
  49. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/tests/GL_PR_BO_JLKU.nc +0 -0
  50. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/tests/conftest.py +0 -0
  51. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/tests/make_fixture.py +0 -0
  52. {atlas_python-0.16.1 → atlas_python-0.16.2}/atlas-python/tests/test_source.py +0 -0
  53. {atlas_python-0.16.1 → atlas_python-0.16.2}/docs/README.md +0 -0
  54. {atlas_python-0.16.1 → atlas_python-0.16.2}/docs/architecture.md +0 -0
  55. {atlas_python-0.16.1 → atlas_python-0.16.2}/docs/data-model.md +0 -0
  56. {atlas_python-0.16.1 → atlas_python-0.16.2}/docs/format.md +0 -0
  57. {atlas_python-0.16.1 → atlas_python-0.16.2}/docs/read-path.md +0 -0
  58. {atlas_python-0.16.1 → atlas_python-0.16.2}/docs/write-path.md +0 -0
  59. {atlas_python-0.16.1 → atlas_python-0.16.2}/examples/lifecycle.rs +0 -0
  60. {atlas_python-0.16.1 → atlas_python-0.16.2}/examples/sensor_fleet.rs +0 -0
  61. {atlas_python-0.16.1 → atlas_python-0.16.2}/examples/weather_store.rs +0 -0
  62. {atlas_python-0.16.1 → atlas_python-0.16.2}/python/atlas/__init__.py +0 -0
  63. {atlas_python-0.16.1 → atlas_python-0.16.2}/python/atlas/_log.py +0 -0
  64. {atlas_python-0.16.1 → atlas_python-0.16.2}/python/atlas/_source.py +0 -0
  65. {atlas_python-0.16.1 → atlas_python-0.16.2}/python/atlas/py.typed +0 -0
  66. {atlas_python-0.16.1 → atlas_python-0.16.2}/python/atlas/xarray.py +0 -0
  67. {atlas_python-0.16.1 → atlas_python-0.16.2}/src/config.rs +0 -0
  68. {atlas_python-0.16.1 → atlas_python-0.16.2}/src/error.rs +0 -0
  69. {atlas_python-0.16.1 → atlas_python-0.16.2}/src/format/footer.rs +0 -0
  70. {atlas_python-0.16.1 → atlas_python-0.16.2}/src/format/mask.rs +0 -0
  71. {atlas_python-0.16.1 → atlas_python-0.16.2}/src/format/mod.rs +0 -0
  72. {atlas_python-0.16.1 → atlas_python-0.16.2}/src/format/segment_store.rs +0 -0
  73. {atlas_python-0.16.1 → atlas_python-0.16.2}/src/lib.rs +0 -0
  74. {atlas_python-0.16.1 → atlas_python-0.16.2}/src/reader/mod.rs +0 -0
  75. {atlas_python-0.16.1 → atlas_python-0.16.2}/src/schema/array.rs +0 -0
  76. {atlas_python-0.16.1 → atlas_python-0.16.2}/src/schema/attr.rs +0 -0
  77. {atlas_python-0.16.1 → atlas_python-0.16.2}/src/schema/dtype.rs +0 -0
  78. {atlas_python-0.16.1 → atlas_python-0.16.2}/src/schema/mod.rs +0 -0
  79. {atlas_python-0.16.1 → atlas_python-0.16.2}/src/writer/mod.rs +0 -0
  80. {atlas_python-0.16.1 → atlas_python-0.16.2}/tests/cross_fixture.rs +0 -0
  81. {atlas_python-0.16.1 → atlas_python-0.16.2}/tests/fixtures/from_python/data.atlas +0 -0
  82. {atlas_python-0.16.1 → atlas_python-0.16.2}/tests/fixtures/golden_v1/data.atlas +0 -0
  83. {atlas_python-0.16.1 → atlas_python-0.16.2}/tests/golden.rs +0 -0
  84. {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.1"
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.1"
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.1"
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.1
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`. It sorts
124
- them, and writes one dataset per file, named after the file. Each coordinate
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
 
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "atlas-python"
3
- version = "0.16.1"
3
+ version = "0.16.2"
4
4
  edition = "2021"
5
5
  readme = "README.md"
6
6
 
@@ -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`. It sorts
96
- them, and writes one dataset per file, named after the file. Each coordinate
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.1: create /data/nc ...
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
- Each NetCDF file becomes one dataset, named after the file. `2024-01.nc`
48
- becomes `2024-01.nc`, suffix and all. The files land in sorted order, which
49
- makes the ordinals of a collection reproducible.
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
- | `-r`, `--recursive` | Descend into subdirectories |
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 --recursive --skip-errors
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
- It **sorts** them, and writes one dataset per file, named after the file.
19
- `2024-01.nc` becomes the dataset `2024-01.nc`. The suffix is part of the name,
20
- so `jan.nc` and `jan.nc4` are two datasets.
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") # sorted list of paths
30
- atlas.find_netcdf_files("/data/nc", recursive=True)
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`. Rename one, or ingest them into two collections.
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 test_create_finds_files_recursively_when_asked(tmp_path):
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
- flat = atlas.find_netcdf_files(tmp_path / "nc")
57
- assert flat == []
57
+ found = atlas.find_netcdf_files(tmp_path / "nc")
58
+ assert [p.name for p in found] == ["jan.nc", "top.nc"]
58
59
 
59
- found = atlas.find_netcdf_files(tmp_path / "nc", recursive=True)
60
- assert [p.name for p in found] == ["jan.nc"]
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
- def test_an_empty_directory_is_an_error(tmp_path):
67
- (tmp_path / "empty").mkdir()
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
- def test_a_missing_directory_is_an_error(tmp_path):
73
- with pytest.raises(atlas.AtlasError, match="not a directory"):
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 test_two_files_with_the_same_name_collide(tmp_path):
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"), recursive=True)
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. It
50
- sorts them, and writes one dataset per file, named after the file. `2024-01.nc`
51
- becomes `2024-01.nc`, suffix and all. The sort makes the ordinals reproducible.
52
- Build the same directory twice, and every dataset lands at the same position.
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
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "atlas-python"
7
- version = "0.16.1"
7
+ version = "0.16.2"
8
8
  description = "Python bindings for the ATLAS array store"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -57,7 +57,7 @@ def create(
57
57
  directory: Union[str, os.PathLike[str]],
58
58
  destination: Source,
59
59
  *,
60
- recursive: bool = False,
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. The files land in sorted
75
- order, which fixes the ordinals of the collection.
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. Off by default.
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 = False
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.recursive,
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("-r", "--recursive", action="store_true", help="descend into subdirectories")
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 = False
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 = False,
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. Nothing at `destination` is readable until
113
- every file lands, with the footer. A failure therefore leaves no half-built
114
- collection.
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