ncfunc 0.1.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.
ncfunc-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,192 @@
1
+ Metadata-Version: 2.3
2
+ Name: ncfunc
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Author: lkkbox
6
+ Author-email: lkkbox <mail@mail.com>
7
+ Requires-Dist: datenum>=0.1.0
8
+ Requires-Dist: netcdf4>=1.7.3
9
+ Requires-Dist: numpy>=1.24
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+
13
+ # ncfunc
14
+
15
+ Functional-style reading and writing of NetCDF data files.
16
+
17
+ `ncfunc` wraps [netCDF4](https://github.com/Unidata/netcdf4-python) and trades
18
+ its open-handle, object-oriented style for small stateless functions: every
19
+ call opens the file, does one thing, closes it, and leaves nothing behind.
20
+ Along the way it decodes CF time coordinates, subsets by coordinate bounds,
21
+ and turns opaque netCDF4 errors into messages that say what failed, where,
22
+ and why - keeping the original exception chained underneath.
23
+
24
+ ## Features
25
+
26
+ - **Stateless functions** - no `Dataset` handles to open, close or leak
27
+ - **Readable errors** - Exceptions are raised with details such as file path,
28
+ variable name, dimension name, etc.
29
+ - **Time decoding** - `read_time` decodes CF `'delta since epoch'` units to
30
+ [datenum](https://pypi.org/project/datenum/) serials, including fixed-length
31
+ calendars (`360_day`, `365_day`)
32
+ - **Bounds-based subsetting** - `read_within` slices variables by coordinate
33
+ ranges instead of index arithmetic
34
+ - **Cached metadata** - structure queries reuse a path-keyed cache that
35
+ auto-refreshes when a file's mtime changes (`DatasetMeta`)
36
+ - **One-shot writer** - `save()` creates dimensions, coordinates and the
37
+ variable in a single call, with overwrite protection
38
+
39
+ ## Installation
40
+
41
+ Requires Python >= 3.10, with dependencies numpy, netCDF4 and datenum installed
42
+ automatically:
43
+
44
+ ```sh
45
+ pip install ncfunc
46
+ # or
47
+ uv add ncfunc
48
+ ```
49
+
50
+ ## Quickstart
51
+
52
+ ```python
53
+ import ncfunc as ncf
54
+
55
+ file = "tests/ersst_2022-2024.nc"
56
+ ```
57
+
58
+ ## API overview
59
+
60
+ | function | purpose |
61
+ | --- | --- |
62
+ | `var_names(path)` | variable names |
63
+ | `dim_names(path, var)` | dimension names of a variable |
64
+ | `shape(path, var)` / `ndim(path, var)` | shape / rank of a variable |
65
+ | `attr_names(path, var)` / `attr_val(path, var, attr)` | attributes of a variable, or of the file with `var='/'` |
66
+ | `var_names_include(...)` | find variables by substring and rank |
67
+ | `attr_names_include(...)` | find attributes by substring |
68
+ | `read(path, var, subsets?)` | read a variable into `np.ndarray` |
69
+ | `read_time(path, ...)` | read and decode a time coordinate to datenum values |
70
+ | `read_within(path, var, withins, ...)` | bounds-based subset read with coordinates |
71
+ | `write(path, var, data, subsets?)` | write into an existing variable |
72
+ | `create(path, var, dim_specs, ...)` | create a variable (+ dimensions), idempotently |
73
+ | `save(path, data, ...)` | create + write a variable and its coordinates in one call |
74
+ | `write_attr(path, var, attr, value)` | set a variable or root (`'/'`) attribute |
75
+ | `DatasetMeta(path)` | static structural snapshot, cached per resolved path |
76
+
77
+
78
+ ### Inspect metadata
79
+
80
+ ```python
81
+ >>> ncf.var_names(file)
82
+ ('time', 'lon', 'lat', 'sst', 'ssta')
83
+
84
+ >>> ncf.shape(file, 'sst')
85
+ (36, 121, 240)
86
+
87
+ >>> ncf.dim_names(file, 'sst')
88
+ ('time', 'lat', 'lon')
89
+
90
+ >>> ncf.attr_val(file, '/', 'title') # '/' selects the file's root attributes
91
+ 'NOAA monthly ERSSTv6 (in situ only)'
92
+ ```
93
+
94
+ Search helpers locate variables and attributes by substring, and insist on an
95
+ unambiguous match (by rank or count) before returning:
96
+
97
+ ```python
98
+ >>> ncf.var_names_include(file, ('sst',), accept_ndims=3)
99
+ ('sst', 'ssta')
100
+
101
+ >>> ncf.var_names_include(file, name_includes=('sst',), accept_ndims=3, accept_counts=(2,))
102
+ ('sst', 'ssta')
103
+ ```
104
+
105
+ ### Read data
106
+
107
+ ```python
108
+ sst = ncf.read(file, 'sst') # whole variable
109
+ top = ncf.read(file, 'sst', ((slice(-4, None),) * 3)) # last 4 steps of every dim
110
+ ```
111
+
112
+ ### Decode time
113
+
114
+ ```python
115
+ >>> import datenum as dn
116
+ >>> t = ncf.read_time(file)
117
+ >>> dn.to_string(t[0]), dn.to_string(t[-1])
118
+ ('2022-01-15 00:00:00', '2024-12-15 00:00:00')
119
+ ```
120
+
121
+ The time variable, its `units` attribute and its `calendar` attribute are all
122
+ guessed; pass `time_name`, `unit_name`, `calendar_name` explicitly to override.
123
+ Month/year-based units decode via month arithmetic, day-based ones against the
124
+ declared calendar - including `360_day` and `365_day` fixed calendars.
125
+
126
+ ### Subset by bounds, not indices
127
+
128
+ `read_within` takes one `(lower, upper)` pair per dimension of the variable
129
+ (`None` = unbounded), reads only what intersects, and returns both the data
130
+ and the bounded coordinates. Coordinates come back ascending even when stored
131
+ descending; the data is flipped to stay aligned:
132
+
133
+ ```python
134
+ >>> sst, (time, lat, lon) = ncf.read_within(
135
+ ... file,
136
+ ... 'sst',
137
+ ... withins=((None, None), (-30.0, 30.0), (150.0, 210.0)),
138
+ ... )
139
+ >>> sst.shape, lat[0], lat[-1]
140
+ ((36, 41, 41), -30.0, 30.0)
141
+ ```
142
+
143
+ The time dimension is found by guessing a time-named coordinate; point at it
144
+ explicitly with `idim_time=<index>` if the guess would be wrong, or disable
145
+ time handling with `decode_time=False`.
146
+
147
+ ### Write data
148
+
149
+ `save` writes a variable plus its dimensions in one shot. The first entry is
150
+ the variable, the rest are its 1-D dimensions:
151
+
152
+ ```python
153
+ import numpy as np
154
+
155
+ ncf.save(
156
+ "out.nc",
157
+ {
158
+ "tas": np.arange(12, dtype="f4").reshape(3, 4),
159
+ "time": np.array([0, 31, 59]),
160
+ "lon": np.linspace(0.5, 3.5, 4),
161
+ },
162
+ )
163
+ ```
164
+
165
+ This creates `out.nc` with dimensions `time` and `lon`, coordinate variables
166
+ stamped with CF-ish attributes (`axis`, `units`, `standard_name`), and the
167
+ compressed `tas` variable. If `tas` already exists in the file, `save` asks
168
+ for confirmation on the terminal; `overwrite_var=True` and `overwrite_dim=True`
169
+ skips the question.
170
+
171
+ For finer control, use the pieces directly:
172
+
173
+ ```python
174
+ ncf.create(path, "tas", {"time": 3, "lon": 4}) # idempotent; missing dims created
175
+ ncf.write(path, "tas", data) # full write, shape must match
176
+ ncf.write(path, "tas", data, ((slice(0, 1), slice(None)),)) # or by slices
177
+ ncf.write_attr(path, "/", "history", "created today") # '/' = root attribute
178
+ ```
179
+
180
+ ## Development
181
+
182
+ ```sh
183
+ uv sync # install dependencies
184
+ uv run pytest # run the test suite
185
+ uv run ruff check src/ tests/
186
+ uv run ruff format --check src/ tests/
187
+ uv run ty check src/ tests/
188
+ ```
189
+
190
+ ## License
191
+
192
+ MIT - see [LICENSE](LICENSE).
ncfunc-0.1.0/README.md ADDED
@@ -0,0 +1,180 @@
1
+ # ncfunc
2
+
3
+ Functional-style reading and writing of NetCDF data files.
4
+
5
+ `ncfunc` wraps [netCDF4](https://github.com/Unidata/netcdf4-python) and trades
6
+ its open-handle, object-oriented style for small stateless functions: every
7
+ call opens the file, does one thing, closes it, and leaves nothing behind.
8
+ Along the way it decodes CF time coordinates, subsets by coordinate bounds,
9
+ and turns opaque netCDF4 errors into messages that say what failed, where,
10
+ and why - keeping the original exception chained underneath.
11
+
12
+ ## Features
13
+
14
+ - **Stateless functions** - no `Dataset` handles to open, close or leak
15
+ - **Readable errors** - Exceptions are raised with details such as file path,
16
+ variable name, dimension name, etc.
17
+ - **Time decoding** - `read_time` decodes CF `'delta since epoch'` units to
18
+ [datenum](https://pypi.org/project/datenum/) serials, including fixed-length
19
+ calendars (`360_day`, `365_day`)
20
+ - **Bounds-based subsetting** - `read_within` slices variables by coordinate
21
+ ranges instead of index arithmetic
22
+ - **Cached metadata** - structure queries reuse a path-keyed cache that
23
+ auto-refreshes when a file's mtime changes (`DatasetMeta`)
24
+ - **One-shot writer** - `save()` creates dimensions, coordinates and the
25
+ variable in a single call, with overwrite protection
26
+
27
+ ## Installation
28
+
29
+ Requires Python >= 3.10, with dependencies numpy, netCDF4 and datenum installed
30
+ automatically:
31
+
32
+ ```sh
33
+ pip install ncfunc
34
+ # or
35
+ uv add ncfunc
36
+ ```
37
+
38
+ ## Quickstart
39
+
40
+ ```python
41
+ import ncfunc as ncf
42
+
43
+ file = "tests/ersst_2022-2024.nc"
44
+ ```
45
+
46
+ ## API overview
47
+
48
+ | function | purpose |
49
+ | --- | --- |
50
+ | `var_names(path)` | variable names |
51
+ | `dim_names(path, var)` | dimension names of a variable |
52
+ | `shape(path, var)` / `ndim(path, var)` | shape / rank of a variable |
53
+ | `attr_names(path, var)` / `attr_val(path, var, attr)` | attributes of a variable, or of the file with `var='/'` |
54
+ | `var_names_include(...)` | find variables by substring and rank |
55
+ | `attr_names_include(...)` | find attributes by substring |
56
+ | `read(path, var, subsets?)` | read a variable into `np.ndarray` |
57
+ | `read_time(path, ...)` | read and decode a time coordinate to datenum values |
58
+ | `read_within(path, var, withins, ...)` | bounds-based subset read with coordinates |
59
+ | `write(path, var, data, subsets?)` | write into an existing variable |
60
+ | `create(path, var, dim_specs, ...)` | create a variable (+ dimensions), idempotently |
61
+ | `save(path, data, ...)` | create + write a variable and its coordinates in one call |
62
+ | `write_attr(path, var, attr, value)` | set a variable or root (`'/'`) attribute |
63
+ | `DatasetMeta(path)` | static structural snapshot, cached per resolved path |
64
+
65
+
66
+ ### Inspect metadata
67
+
68
+ ```python
69
+ >>> ncf.var_names(file)
70
+ ('time', 'lon', 'lat', 'sst', 'ssta')
71
+
72
+ >>> ncf.shape(file, 'sst')
73
+ (36, 121, 240)
74
+
75
+ >>> ncf.dim_names(file, 'sst')
76
+ ('time', 'lat', 'lon')
77
+
78
+ >>> ncf.attr_val(file, '/', 'title') # '/' selects the file's root attributes
79
+ 'NOAA monthly ERSSTv6 (in situ only)'
80
+ ```
81
+
82
+ Search helpers locate variables and attributes by substring, and insist on an
83
+ unambiguous match (by rank or count) before returning:
84
+
85
+ ```python
86
+ >>> ncf.var_names_include(file, ('sst',), accept_ndims=3)
87
+ ('sst', 'ssta')
88
+
89
+ >>> ncf.var_names_include(file, name_includes=('sst',), accept_ndims=3, accept_counts=(2,))
90
+ ('sst', 'ssta')
91
+ ```
92
+
93
+ ### Read data
94
+
95
+ ```python
96
+ sst = ncf.read(file, 'sst') # whole variable
97
+ top = ncf.read(file, 'sst', ((slice(-4, None),) * 3)) # last 4 steps of every dim
98
+ ```
99
+
100
+ ### Decode time
101
+
102
+ ```python
103
+ >>> import datenum as dn
104
+ >>> t = ncf.read_time(file)
105
+ >>> dn.to_string(t[0]), dn.to_string(t[-1])
106
+ ('2022-01-15 00:00:00', '2024-12-15 00:00:00')
107
+ ```
108
+
109
+ The time variable, its `units` attribute and its `calendar` attribute are all
110
+ guessed; pass `time_name`, `unit_name`, `calendar_name` explicitly to override.
111
+ Month/year-based units decode via month arithmetic, day-based ones against the
112
+ declared calendar - including `360_day` and `365_day` fixed calendars.
113
+
114
+ ### Subset by bounds, not indices
115
+
116
+ `read_within` takes one `(lower, upper)` pair per dimension of the variable
117
+ (`None` = unbounded), reads only what intersects, and returns both the data
118
+ and the bounded coordinates. Coordinates come back ascending even when stored
119
+ descending; the data is flipped to stay aligned:
120
+
121
+ ```python
122
+ >>> sst, (time, lat, lon) = ncf.read_within(
123
+ ... file,
124
+ ... 'sst',
125
+ ... withins=((None, None), (-30.0, 30.0), (150.0, 210.0)),
126
+ ... )
127
+ >>> sst.shape, lat[0], lat[-1]
128
+ ((36, 41, 41), -30.0, 30.0)
129
+ ```
130
+
131
+ The time dimension is found by guessing a time-named coordinate; point at it
132
+ explicitly with `idim_time=<index>` if the guess would be wrong, or disable
133
+ time handling with `decode_time=False`.
134
+
135
+ ### Write data
136
+
137
+ `save` writes a variable plus its dimensions in one shot. The first entry is
138
+ the variable, the rest are its 1-D dimensions:
139
+
140
+ ```python
141
+ import numpy as np
142
+
143
+ ncf.save(
144
+ "out.nc",
145
+ {
146
+ "tas": np.arange(12, dtype="f4").reshape(3, 4),
147
+ "time": np.array([0, 31, 59]),
148
+ "lon": np.linspace(0.5, 3.5, 4),
149
+ },
150
+ )
151
+ ```
152
+
153
+ This creates `out.nc` with dimensions `time` and `lon`, coordinate variables
154
+ stamped with CF-ish attributes (`axis`, `units`, `standard_name`), and the
155
+ compressed `tas` variable. If `tas` already exists in the file, `save` asks
156
+ for confirmation on the terminal; `overwrite_var=True` and `overwrite_dim=True`
157
+ skips the question.
158
+
159
+ For finer control, use the pieces directly:
160
+
161
+ ```python
162
+ ncf.create(path, "tas", {"time": 3, "lon": 4}) # idempotent; missing dims created
163
+ ncf.write(path, "tas", data) # full write, shape must match
164
+ ncf.write(path, "tas", data, ((slice(0, 1), slice(None)),)) # or by slices
165
+ ncf.write_attr(path, "/", "history", "created today") # '/' = root attribute
166
+ ```
167
+
168
+ ## Development
169
+
170
+ ```sh
171
+ uv sync # install dependencies
172
+ uv run pytest # run the test suite
173
+ uv run ruff check src/ tests/
174
+ uv run ruff format --check src/ tests/
175
+ uv run ty check src/ tests/
176
+ ```
177
+
178
+ ## License
179
+
180
+ MIT - see [LICENSE](LICENSE).
@@ -0,0 +1,34 @@
1
+ [project]
2
+ name = "ncfunc"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ dependencies = [
8
+ "datenum>=0.1.0",
9
+ "netcdf4>=1.7.3",
10
+ "numpy>=1.24",
11
+ ]
12
+
13
+ [[project.authors]]
14
+ name = "lkkbox"
15
+ email = "mail@mail.com"
16
+
17
+ [project.scripts]
18
+ ncfunc = "ncfunc:main"
19
+
20
+ [build-system]
21
+ requires = ["uv_build>=0.12.1,<0.13.0"]
22
+ build-backend = "uv_build"
23
+
24
+ [dependency-groups]
25
+ dev = ["pytest>=9.1.1"]
26
+
27
+ [tool.ruff.lint]
28
+ extend-select = [
29
+ "N802",
30
+ "N803",
31
+ "N806",
32
+ "N815",
33
+ "N816",
34
+ ]
@@ -0,0 +1,29 @@
1
+ [project]
2
+ name = "ncfunc"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "lkkbox", email = "mail@mail.com" }
8
+ ]
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "datenum>=0.1.0",
12
+ "netcdf4>=1.7.3",
13
+ "numpy>=1.24",
14
+ ]
15
+
16
+ [project.scripts]
17
+ ncfunc = "ncfunc:main"
18
+
19
+ [build-system]
20
+ requires = ["uv_build>=0.12.1,<0.13.0"]
21
+ build-backend = "uv_build"
22
+
23
+ [dependency-groups]
24
+ dev = [
25
+ "pytest>=9.1.1",
26
+ ]
27
+
28
+ [tool.ruff.lint]
29
+ extend-select = ["N802", "N803", "N806", "N815", "N816"]
@@ -0,0 +1,37 @@
1
+ from .core import (
2
+ DatasetMeta,
3
+ attr_names,
4
+ attr_names_include,
5
+ attr_val,
6
+ create,
7
+ dim_names,
8
+ ndim,
9
+ read,
10
+ read_time,
11
+ read_within,
12
+ save,
13
+ shape,
14
+ var_names,
15
+ var_names_include,
16
+ write,
17
+ write_attr,
18
+ )
19
+
20
+ __all__ = [
21
+ "DatasetMeta",
22
+ "attr_names",
23
+ "attr_names_include",
24
+ "attr_val",
25
+ "create",
26
+ "dim_names",
27
+ "ndim",
28
+ "read",
29
+ "read_time",
30
+ "read_within",
31
+ "save",
32
+ "shape",
33
+ "var_names",
34
+ "var_names_include",
35
+ "write",
36
+ "write_attr",
37
+ ]