ro-climate 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.
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
6
+ and the project uses [Semantic Versioning](https://semver.org/).
7
+
8
+ ## [0.1.0] - 2026-11-07
9
+
10
+ ### Added
11
+
12
+ - Point time series and area summaries from the ANM daily gridded
13
+ meteorological cube for Romania.
14
+ - `RoClimate`, `RoClimatePoint`, and `RoClimateArea` public API.
15
+ - Statistics, resampling, climatologies, climate indices, and narrative
16
+ climate-description helpers.
17
+ - Chunk-granular area reads and a persistent raw Zarr chunk cache.
18
+ - CSV, JSON, NPZ, and zip exports.
19
+ - Offline synthetic test suite.
20
+
21
+ [Unreleased]: https://github.com/strainu/ro_climate/compare/v0.1.0...HEAD
22
+ [0.1.0]: https://github.com/strainu/ro_climate/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Strainu and ro-climate contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,4 @@
1
+ include LICENSE
2
+ include README.md
3
+ include CHANGELOG.md
4
+ recursive-include tests *.py
@@ -0,0 +1,539 @@
1
+ Metadata-Version: 2.4
2
+ Name: ro-climate
3
+ Version: 0.1.0
4
+ Summary: Romanian daily gridded climate data from ANM
5
+ Author-email: Strainu <wiki@strainu.ro>
6
+ Maintainer-email: Strainu <wiki@strainu.ro>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/strainu/ro_climate
9
+ Project-URL: Documentation, https://github.com/strainu/ro_climate#readme
10
+ Project-URL: Repository, https://github.com/strainu/ro_climate
11
+ Project-URL: Issues, https://github.com/strainu/ro_climate/issues
12
+ Project-URL: Changelog, https://github.com/strainu/ro_climate/blob/main/CHANGELOG.md
13
+ Keywords: climate,meteorology,romania,anm,weather,gridded-data,temperature,precipitation,zarr
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
24
+ Classifier: Topic :: Scientific/Engineering
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.10
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Requires-Dist: numpy>=1.23
30
+ Requires-Dist: pandas>=2.0
31
+ Requires-Dist: requests>=2.28
32
+ Requires-Dist: zarr<3,>=2.18
33
+ Provides-Extra: test
34
+ Requires-Dist: pytest>=8; extra == "test"
35
+ Provides-Extra: dev
36
+ Requires-Dist: pytest>=8; extra == "dev"
37
+ Requires-Dist: build>=1.2; extra == "dev"
38
+ Requires-Dist: twine>=5; extra == "dev"
39
+ Dynamic: license-file
40
+
41
+ # ro-climate — ANM daily gridded meteorology
42
+
43
+ [![PyPI](https://img.shields.io/pypi/v/ro-climate.svg)](https://pypi.org/project/ro-climate/)
44
+ [![Python versions](https://img.shields.io/pypi/pyversions/ro-climate.svg)](https://pypi.org/project/ro-climate/)
45
+ [![CI](https://github.com/strainu/ro_climate/actions/workflows/ci.yml/badge.svg)](https://github.com/strainu/ro_climate/actions/workflows/ci.yml)
46
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/strainu/ro_climate/blob/main/LICENSE)
47
+
48
+ `ro-climate` is a small, object-oriented Python client for the public **ANM
49
+ daily gridded meteorological cube** for Romania. It reads single grid cells or
50
+ rectangular/polygon areas for an optional date range, returns pandas time
51
+ series or daily spatial summaries, and provides statistics, time aggregation,
52
+ climate indices, exports, and a local chunk cache.
53
+
54
+ - Source code: <https://github.com/strainu/ro_climate>
55
+ - Issue tracker: <https://github.com/strainu/ro_climate/issues>
56
+ - Changelog: [CHANGELOG.md](https://github.com/strainu/ro_climate/blob/main/CHANGELOG.md)
57
+ - Store: <https://storage.svc.uvt-01.eocube.ro/meteo/cube/open-v1/>
58
+ - STAC item: <https://stac.eocube.ro/collections/anm-daily-grids/items/anm-daily-grids-open-v1>
59
+ - Variables: `max_temp` (°C), `min_temp` (°C), `precipitation` (mm)
60
+ - Data licence: CC BY 4.0
61
+
62
+ > **Data attribution.** The data are published by Administrația Națională de
63
+ > Meteorologie (MeteoRomania), via data.gov.ro, under CC BY 4.0; cube assembly
64
+ > and hosting are by West University of Timișoara (ROCS / EOCube.RO). Any
65
+ > publication using the data must preserve this attribution.
66
+
67
+ ## Installation
68
+
69
+ `ro-climate` requires Python 3.10 or newer.
70
+
71
+ ```bash
72
+ python -m pip install ro-climate
73
+ ```
74
+
75
+ For development, install the test and release tools as well:
76
+
77
+ ```bash
78
+ python -m pip install -e ".[dev]" # pytest, build, twine
79
+ python -m pip install -e ".[test]" # pytest only
80
+ ```
81
+
82
+ Runtime dependencies are deliberately small: `zarr` (v2), `numpy`, `pandas`,
83
+ and `requests`. No `xarray`, `fsspec`, `aiohttp`, or plotting stack is needed.
84
+
85
+ ## Quick start
86
+
87
+ ```python
88
+ from ro_climate import RoClimate
89
+
90
+ with RoClimate() as client:
91
+ point = client.point(
92
+ 26.1025,
93
+ 44.4325,
94
+ start="2020-01-01",
95
+ end="2020-12-31",
96
+ )
97
+ print(point)
98
+ print(point.mean()) # mean of each variable
99
+ print(point.max("max_temp")) # hottest day in the period
100
+ print(point.sum("precipitation")) # total precipitation
101
+ ```
102
+
103
+ > **Performance note.** The cube stores 90-day × 161-row × 324-column chunks,
104
+ > so a single point's full 1961–present history requires downloading the
105
+ > chunks covering that point for every 90-day period. The first uncached
106
+ > full-history request can therefore transfer several gigabytes and take
107
+ > minutes per variable. For interactive work, pass a narrower `start`/`end`
108
+ > and select only the variables you need. Area requests are chunk-aligned and
109
+ > read each intersecting chunk once instead of repeating a point download for
110
+ > every cell. Point series and area summaries are not archived; the raw Zarr
111
+ > chunk cache (enabled by default) makes later overlapping requests local.
112
+
113
+ The requested coordinate is snapped to the containing 0.01° grid cell. The
114
+ result records the cell indices, the cell centre, and the cell bounds:
115
+
116
+ ```python
117
+ print(point.i, point.j)
118
+ print(point.grid_lon, point.grid_lat)
119
+ print(point.cell_bounds) # (lon_min, lon_max, lat_min, lat_max)
120
+ ```
121
+
122
+ The cube uses a 0.01° grid with cell centres from 20.135°E to 29.845°E and
123
+ 48.355°N to 43.535°N. Coordinates outside the grid raise `OutsideGridError`.
124
+
125
+ ## Date ranges and frontiers
126
+
127
+ - Without `start`, a point starts at the first cube day, **1961-01-01**.
128
+ - Without `end`, it ends at the latest day that is available for **all**
129
+ requested variables.
130
+ - Variable frontiers are read from the authoritative `last_*_time` arrays,
131
+ not from the STAC `end_datetime` field, which can lag behind the data.
132
+ - An `end` later than the common frontier is clamped and reported in
133
+ `point.warnings`. Intersecting frontiers avoids returning the zero-filled
134
+ preallocated days beyond each variable's last written value.
135
+
136
+ ```python
137
+ point = client.point(26.1025, 44.4325, start="2015-06-01")
138
+ print(point.start, point.end, point.warnings)
139
+ ```
140
+
141
+ ## Statistics
142
+
143
+ ```python
144
+ point.mean() # pandas Series, one value per loaded variable
145
+ point.avg() # alias of mean()
146
+ point.max()
147
+ point.min()
148
+ point.median()
149
+ point.std()
150
+ point.var()
151
+ point.quantile(0.9)
152
+ point.count()
153
+ point.describe() # count/mean/std/min/... plus missing and sum
154
+ point.coverage # valid/missing counts and fraction per variable
155
+ ```
156
+
157
+ `sum()` is only defined for additive variables. It defaults to precipitation
158
+ and raises `NonAdditiveVariableError` for temperature:
159
+
160
+ ```python
161
+ point.sum() # precipitation total
162
+ point.sum("precipitation")
163
+ point.sum("max_temp") # raises NonAdditiveVariableError
164
+ point.sum("max_temp", allow_non_additive=True) # explicit override
165
+ ```
166
+
167
+ All statistics skip missing values by default (`skipna=True`).
168
+
169
+ ## Time aggregation
170
+
171
+ ```python
172
+ point.monthly_mean() # month-start index
173
+ point.monthly_total() # precipitation totals
174
+ point.annual_mean()
175
+ point.annual_total()
176
+ point.annual_extremes() # yearly max and min for each variable
177
+ point.extreme_dates() # min/max values and their dates
178
+ point.annual_mean_temperature() # yearly means of (max_temp + min_temp) / 2
179
+ point.monthly_mean_temperature() # climatological monthly means
180
+ point.warmest_month() # (month, mean temperature)
181
+ point.coldest_month() # (month, mean temperature)
182
+ point.wettest_year() # (year, annual precipitation)
183
+ point.driest_year() # (year, annual precipitation)
184
+ point.precipitation_summary() # mean, wettest and driest year
185
+ point.climatology("month") # mean by calendar month
186
+ point.climatology("dayofyear") # mean by day of year
187
+
188
+ point.resample("MS", "mean") # any pandas offset alias
189
+ point.resample("YS", "max")
190
+ ```
191
+
192
+ `resample(..., how="sum")` is rejected for temperature, just like `sum()`.
193
+
194
+ ### Filtered queries in one line
195
+
196
+ `RoClimatePoint.aggregate()` combines date filtering, month/season filtering, and a
197
+ reduction. For example, the maximum temperature in January over a date range:
198
+
199
+ ```python
200
+ point = client.point(26.1025, 44.4325, start="2010-01-01", end="2020-12-31")
201
+ jan_max = point.aggregate("max_temp", "max", months=1)
202
+ print(jan_max)
203
+
204
+ # Same query, one value per year
205
+ per_year = point.aggregate("max_temp", "max", months=1, groupby="year")
206
+
207
+ # Seasons: DJF, MAM, JJA, SON
208
+ jja_mean = point.aggregate("max_temp", "mean", season="JJA")
209
+ winter = point.aggregate(
210
+ "precipitation", "sum", season="DJF", groupby="season_year"
211
+ )
212
+ ```
213
+
214
+ `groupby` accepts `"year"`, `"month"`, `"season_year"`, or a pandas offset
215
+ alias such as `"YS"` and `"MS"`. `season_year` assigns December to the
216
+ following winter.
217
+
218
+ To keep several variables or apply custom pandas logic, use `subset()`:
219
+
220
+ ```python
221
+ jan = point.subset(start="2020-01-01", end="2020-01-31", months=1)
222
+ print(jan.data)
223
+ ```
224
+
225
+ ## Derived values and climate indices
226
+
227
+ ```python
228
+ point.tmean # (max_temp + min_temp) / 2
229
+ point.diurnal_range # max_temp - min_temp
230
+ point.mean_temperature()
231
+ point.mean_diurnal_range()
232
+
233
+ point.frost_days() # min_temp < 0 °C
234
+ point.summer_days() # max_temp >= 25 °C
235
+ point.hot_days() # max_temp >= 30 °C
236
+ point.tropical_nights() # min_temp >= 20 °C
237
+ point.rain_days() # precipitation >= 1 mm
238
+ point.heavy_rain_days() # precipitation >= 10 mm
239
+ point.dry_days() # precipitation < 1 mm
240
+ point.max_daily_precip()
241
+ point.max_n_day_precip(5) # max rolling 5-day precipitation total
242
+ point.growing_degree_days(10) # base 10 °C, mean-temperature method
243
+ point.consecutive_dry_days()
244
+ point.consecutive_wet_days()
245
+ ```
246
+
247
+ Most threshold indices accept a custom `threshold` and a `by` argument:
248
+
249
+ ```python
250
+ point.hot_days(30, by="year")
251
+ point.rain_days(1.0, by="month")
252
+ point.frost_days(0.0, by="season_year")
253
+ point.hot_days(30, by="YS") # any pandas offset alias
254
+ ```
255
+
256
+ ### Climate description helpers
257
+
258
+ These return structured values for narrative summaries:
259
+
260
+ ```python
261
+ point.extreme_dates(["min_temp", "max_temp"])
262
+ # min min_date max max_date
263
+ # min_temp ... ... ... ...
264
+ # max_temp ... ... ... ...
265
+
266
+ point.annual_mean_temperature() # yearly means of (max_temp + min_temp) / 2
267
+ point.monthly_mean_temperature() # climatological monthly means
268
+ point.warmest_month(), point.coldest_month()
269
+ point.precipitation_summary()
270
+ # {'mean_annual': ..., 'wettest': (year, value), 'driest': (year, value)}
271
+ ```
272
+
273
+ `extreme_dates()` reports the value and date of the minimum and maximum. For a
274
+ point, the index is the variable; values are computed over the loaded period.
275
+
276
+ These helpers operate on the existing summary series. Spatial ranges across a
277
+ county require per-cell aggregation and are not provided by the area object.
278
+
279
+ ## Area and grid summaries
280
+
281
+ `area()` reads a rectangular window and returns **daily spatial summaries**,
282
+ not raw grids. Bounds are `(west, south, east, north)` in WGS84 degrees:
283
+
284
+ ```python
285
+ area = client.area(
286
+ bounds=(25.90, 44.30, 26.30, 44.60),
287
+ start="2020-01-01",
288
+ end="2020-12-31",
289
+ variables=["max_temp", "precipitation"],
290
+ reductions=["mean", "max"],
291
+ )
292
+ print(area.data.head())
293
+ print(area.mean()) # temporal mean of each daily summary
294
+ print(area.series("max_temp", "max"))
295
+ ```
296
+
297
+ Supported spatial reductions are `mean` (the default), `min`, `max`, `sum`,
298
+ and `count`. Data columns are named `{variable}_{reduction}`, for example
299
+ `max_temp_mean` or `precipitation_count`.
300
+
301
+ Pass `polygon=` to keep only cells whose centres fall inside a simple polygon.
302
+ The polygon's bounding box is used as the search window unless `bounds=` is
303
+ also given, in which case the polygon acts as a mask within the window:
304
+
305
+ ```python
306
+ area = client.area(
307
+ polygon=[
308
+ (26.00, 44.30),
309
+ (26.30, 44.30),
310
+ (26.30, 44.60),
311
+ (26.00, 44.60),
312
+ ],
313
+ start="2020-06-01",
314
+ end="2020-08-31",
315
+ )
316
+ ```
317
+
318
+ Holes and multipart polygons are not supported. Selection is by cell centre;
319
+ there is no exact area weighting.
320
+
321
+ `min_count` is the minimum number of valid cells required for a non-count
322
+ reduction (default `1`). Spatial `sum` is limited to additive variables
323
+ (precipitation) unless `allow_non_additive=True` is passed:
324
+
325
+ ```python
326
+ dry_cells = client.area(
327
+ bounds=(25.90, 44.30, 26.30, 44.60),
328
+ start="2020-01-01",
329
+ reductions=["mean", "count"],
330
+ min_count=10,
331
+ )
332
+
333
+ rain = client.area(
334
+ bounds=(25.90, 44.30, 26.30, 44.60),
335
+ start="2020-01-01",
336
+ variables="precipitation",
337
+ reductions="sum",
338
+ )
339
+ ```
340
+
341
+ The returned `RoClimateArea` exposes:
342
+
343
+ ```python
344
+ area.data # daily summaries, one row per day
345
+ area.bounds # requested west/south/east/north
346
+ area.i0, area.i1 # inclusive column indices
347
+ area.j0, area.j1 # inclusive row indices
348
+ area.n_cells # selected cells (after polygon masking)
349
+ area.shape # (rows, columns) of the grid window
350
+ area.variables, area.reductions, area.columns
351
+ area.coverage # valid cell-days per variable
352
+ area.warnings # clipping and clamping notices
353
+ ```
354
+
355
+ `.coverage` covers the requested date range and the selected cells.
356
+
357
+ It offers the same style of access and aggregation as a point:
358
+
359
+ ```python
360
+ area.at("2020-07-01")
361
+ area.subset(start="2020-06-01", end="2020-08-31", months=[6, 7, 8])
362
+ area["max_temp_mean"]
363
+ area[["max_temp_mean", "precipitation_mean"]]
364
+ area.mean(), area.max(), area.min(), area.median()
365
+ area.std(), area.var(), area.count(), area.quantile(0.9)
366
+ area.sum(), area.describe()
367
+
368
+ area.resample("MS", "mean")
369
+ area.monthly_mean(), area.monthly_total()
370
+ area.annual_mean(), area.annual_total()
371
+ area.annual_extremes()
372
+ area.extreme_dates()
373
+ area.climatology("month"), area.climatology("dayofyear")
374
+ area.aggregate("max_temp", "max", how="mean", months=1, groupby="year")
375
+
376
+ area.tmean # daily spatial mean temperature
377
+ area.mean_temperature()
378
+ area.annual_mean_temperature()
379
+ area.monthly_mean_temperature()
380
+ area.warmest_month(), area.coldest_month()
381
+ area.wettest_year(), area.driest_year()
382
+ area.precipitation_summary()
383
+ ```
384
+
385
+ `area.tmean` is `(max_temp_mean + min_temp_mean) / 2`, so both temperature
386
+ variables must be loaded with the default `mean` reduction.
387
+
388
+ ### Area reads
389
+
390
+ Area summaries are not stored. Each call reads the chunks intersecting the
391
+ selected cells and computes the requested reductions.
392
+
393
+ ## Local chunk cache
394
+
395
+ The only cache is the raw compressed Zarr chunk cache, enabled by default:
396
+
397
+ ```python
398
+ client = RoClimate(
399
+ chunk_cache_dir="ro-climate-cache",
400
+ chunk_cache_max_mb=2048,
401
+ )
402
+ ```
403
+
404
+ Compressed Zarr chunks are stored under
405
+ `<chunk_cache_dir>/chunks/<store digest>/`. They are shared by point and area
406
+ requests. The cache is capped at **20 GB** by default. Set
407
+ `chunk_cache_max_mb=` or the `RO_CLIMATE_CHUNK_CACHE_MAX_MB` environment
408
+ variable to change it; `0` disables the limit. The oldest chunks are evicted
409
+ when the cap is exceeded.
410
+
411
+ ```python
412
+ from ro_climate import RoClimate
413
+
414
+ client = RoClimate(chunk_cache_dir="ro-climate-cache")
415
+ point = client.point(26.1025, 44.4325, start="2020-01-01", end="2020-12-31")
416
+ area = client.area(
417
+ bounds=(25.0, 44.0, 27.0, 45.0), start="2020-01-01", end="2020-12-31"
418
+ )
419
+
420
+ print(client.chunk_cache_info())
421
+ client.prune_cache(older_than_days=30)
422
+ client.clear_cache()
423
+ client.close()
424
+ ```
425
+
426
+ Set `RO_CLIMATE_CACHE_DIR` to override the platform cache root and
427
+ `RO_CLIMATE_CHUNK_CACHE_MAX_MB` to override the size cap. Pass
428
+ `chunk_cache=False` to disable local storage, or `refresh=True` to a `point()`
429
+ or `area()` call to invalidate the chunks covering the requested range.
430
+
431
+ ## Exports
432
+
433
+ ```python
434
+ point.to_csv("bucharest_2020.csv")
435
+ point.to_json("bucharest_2020.json")
436
+ point.to_npz("bucharest_2020.npz")
437
+ point.to_zip("bucharest_2020.zip") # same archive layout
438
+ print(point.metadata())
439
+
440
+ area.to_csv("muntenia_2020.csv")
441
+ area.to_json("muntenia_2020.json")
442
+ area.to_npz("muntenia_2020.npz")
443
+ area.to_zip("muntenia_2020.zip")
444
+ print(area.metadata())
445
+ ```
446
+
447
+ `to_csv()` writes the date index with the column name `date` and formats
448
+ floats to four decimals by default. Extra keyword arguments are passed to
449
+ pandas, so override `float_format` or use `sep=";"` as needed.
450
+
451
+ ## API summary
452
+
453
+ `RoClimate(...)`
454
+
455
+ - `RoClimate(store_url=None, *, discover=True, chunk_cache=True,
456
+ chunk_cache_dir=None, chunk_cache_max_mb=None, timeout=30.0, retries=3,
457
+ frontier_ttl=3600)`
458
+ - `.point(lon, lat, start=None, end=None, variables=None, *, refresh=None,
459
+ strict=False) -> RoClimatePoint`
460
+ - `.area(bounds=None, start=None, end=None, variables=None, reductions=None,
461
+ *, polygon=None, min_count=1, allow_non_additive=False, refresh=None,
462
+ strict=False) -> RoClimateArea`
463
+ - `.frontiers(force=False)` — last written day per variable
464
+ - `.chunk_cache_info()`, `.clear_cache()`, `.prune_cache(older_than_days=30)`
465
+ - Context manager and `.close()`
466
+
467
+ `RoClimatePoint`
468
+
469
+ - Access: `.data`, `.variables`, `.units`, `.start`, `.end`, `.i`, `.j`,
470
+ `.grid_lon`, `.grid_lat`, `.cell_bounds`, `.coverage`, `.warnings`
471
+ - Selection: `.at(date)`, `.subset(...)`, `point["max_temp"]`,
472
+ `point[["max_temp", "precipitation"]]`
473
+ - Statistics: `.mean/.avg/.max/.min/.median/.std/.var/.quantile/.count/
474
+ .sum/.describe`
475
+ - Aggregation: `.resample`, `.monthly_mean`, `.monthly_total`,
476
+ `.annual_mean`, `.annual_total`, `.annual_extremes`, `.extreme_dates`,
477
+ `.annual_mean_temperature`, `.monthly_mean_temperature`,
478
+ `.warmest_month`, `.coldest_month`, `.wettest_year`, `.driest_year`,
479
+ `.precipitation_summary`, `.climatology`, `.aggregate`
480
+ - Indices and derived values as listed above
481
+ - Exports: `.to_csv`, `.to_json`, `.to_npz`, `.to_zip`, `.metadata`
482
+
483
+ `RoClimateArea`
484
+
485
+ - Access: `.data`, `.bounds`, `.i0/.i1/.j0/.j1`, `.shape`, `.n_cells`,
486
+ `.variables`, `.reductions`, `.columns`, `.polygon`, `.coverage`,
487
+ `.warnings`, `.tmean`
488
+ - Selection: `.at(date)`, `.subset(...)`, `.series(variable, reduction)`,
489
+ `area["max_temp_mean"]`, `area[["max_temp_mean", "max_temp_max"]]`
490
+ - Statistics over daily spatial summaries: `.mean/.avg/.max/.min/.median/
491
+ .std/.var/.quantile/.count/.sum/.describe`, `.mean_temperature`
492
+ - Aggregation: `.resample`, `.monthly_mean`, `.monthly_total`,
493
+ `.annual_mean`, `.annual_total`, `.annual_extremes`, `.extreme_dates`,
494
+ `.annual_mean_temperature`, `.monthly_mean_temperature`,
495
+ `.warmest_month`, `.coldest_month`, `.wettest_year`, `.driest_year`,
496
+ `.precipitation_summary`, `.climatology`, `.aggregate`
497
+ - Exports: `.to_csv`, `.to_json`, `.to_npz`, `.to_zip`, `.metadata`
498
+
499
+ ## Tests
500
+
501
+ From this directory:
502
+
503
+ ```bash
504
+ python -m pytest
505
+ ```
506
+
507
+ The tests use a synthetic in-memory group and never contact the network.
508
+ They cover grid mapping, statistics, resampling, indices, area summaries and
509
+ polygon masks, chunk-aligned reads, the raw chunk cache, the requests-backed
510
+ store, client fetching, and export archives.
511
+
512
+ ## Licence
513
+
514
+ The **source code** of `ro-climate` is released under the [MIT License](https://github.com/strainu/ro_climate/blob/main/LICENSE).
515
+
516
+ The **ANM data** accessed through the library are published under
517
+ [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). The software
518
+ licence does not replace or relax the data licence. When you redistribute
519
+ data obtained through this library, keep the attribution and licence notice
520
+ in the exported metadata and README files.
521
+
522
+ ## Citation
523
+
524
+ If you use the data in a publication, cite the data provider and describe the
525
+ dataset, for example:
526
+
527
+ > Administrația Națională de Meteorologie (ANM), *ANM daily gridded
528
+ > meteorological cube*, CC BY 4.0, via data.gov.ro; cube assembly and hosting
529
+ > by West University of Timișoara (ROCS / EOCube.RO).
530
+
531
+ If you use the library itself, you may also link to
532
+ <https://github.com/strainu/ro_climate>.
533
+
534
+ ## Contributing
535
+
536
+ Bug reports and pull requests are welcome at
537
+ <https://github.com/strainu/ro_climate/issues>. Run `python -m pytest -q`
538
+ before opening a pull request. Tests must remain offline: use synthetic data
539
+ and fake HTTP sessions rather than live downloads.