tonik 0.1.19__py3-none-any.whl → 0.1.21__py3-none-any.whl
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.
- tonik/storage.py +1 -1
- tonik/utils.py +140 -2
- tonik/xarray2zarr.py +9 -3
- {tonik-0.1.19.dist-info → tonik-0.1.21.dist-info}/METADATA +1 -1
- {tonik-0.1.19.dist-info → tonik-0.1.21.dist-info}/RECORD +8 -8
- {tonik-0.1.19.dist-info → tonik-0.1.21.dist-info}/WHEEL +0 -0
- {tonik-0.1.19.dist-info → tonik-0.1.21.dist-info}/entry_points.txt +0 -0
- {tonik-0.1.19.dist-info → tonik-0.1.21.dist-info}/licenses/LICENSE +0 -0
tonik/storage.py
CHANGED
|
@@ -18,7 +18,7 @@ LOGGING_CONFIG = {
|
|
|
18
18
|
"datefmt": "%Y-%m-%d %H:%M:%S", # How to display dates
|
|
19
19
|
},
|
|
20
20
|
"json": { # The formatter name
|
|
21
|
-
"()": "pythonjsonlogger.
|
|
21
|
+
"()": "pythonjsonlogger.json.JsonFormatter", # The class to instantiate!
|
|
22
22
|
# Json is more complex, but easier to read, display all attributes!
|
|
23
23
|
"format": """
|
|
24
24
|
asctime: %(asctime)s
|
tonik/utils.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import List
|
|
1
|
+
from typing import List, Union
|
|
2
2
|
from datetime import datetime, timezone, timedelta
|
|
3
3
|
|
|
4
4
|
import numpy as np
|
|
@@ -13,12 +13,40 @@ def generate_test_data(dim=1, ndays=30, nfreqs=10,
|
|
|
13
13
|
freq_names=None, add_nans=True):
|
|
14
14
|
"""
|
|
15
15
|
Generate a 1D or 2D feature for testing.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
dim : int
|
|
20
|
+
Dimension of the data (1 or 2).
|
|
21
|
+
ndays : int
|
|
22
|
+
Number of days to generate data for.
|
|
23
|
+
nfreqs : int
|
|
24
|
+
Number of frequencies (only for dim=2).
|
|
25
|
+
tstart : datetime
|
|
26
|
+
Start time of the data.
|
|
27
|
+
freq : str
|
|
28
|
+
Frequency of the data (e.g., '10min').
|
|
29
|
+
intervals : int
|
|
30
|
+
Number of intervals to generate. If None, calculated from ndays and freq.
|
|
31
|
+
feature_names : list
|
|
32
|
+
Names of the features to generate.
|
|
33
|
+
seed : int
|
|
34
|
+
Random seed for reproducibility.
|
|
35
|
+
freq_names : list
|
|
36
|
+
Names of the frequency dimensions (only for dim=2).
|
|
37
|
+
add_nans : bool
|
|
38
|
+
Whether to add NaN values to the data.
|
|
39
|
+
|
|
40
|
+
Returns
|
|
41
|
+
-------
|
|
42
|
+
xr.Dataset
|
|
43
|
+
Generated test dataset.
|
|
16
44
|
"""
|
|
17
45
|
assert dim < 3
|
|
18
46
|
assert dim > 0
|
|
19
47
|
|
|
20
48
|
if intervals is None:
|
|
21
|
-
nints = ndays *
|
|
49
|
+
nints = ndays * int(pd.Timedelta('1h')/pd.Timedelta(freq)) * 24
|
|
22
50
|
else:
|
|
23
51
|
nints = intervals
|
|
24
52
|
dates = pd.date_range(tstart, freq=freq, periods=nints)
|
|
@@ -59,6 +87,116 @@ def generate_test_data(dim=1, ndays=30, nfreqs=10,
|
|
|
59
87
|
return xds
|
|
60
88
|
|
|
61
89
|
|
|
90
|
+
def round_datetime(dt: datetime, interval: Union[int, float, timedelta]) -> datetime:
|
|
91
|
+
"""
|
|
92
|
+
Find closest multiple of interval to given time.
|
|
93
|
+
|
|
94
|
+
Parameters:
|
|
95
|
+
-----------
|
|
96
|
+
dt : datetime
|
|
97
|
+
The datetime to round.
|
|
98
|
+
interval : Union[int, float, timedelta]
|
|
99
|
+
The interval to which to round the datetime.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
--------
|
|
103
|
+
datetime
|
|
104
|
+
The rounded datetime.
|
|
105
|
+
"""
|
|
106
|
+
# Normalize interval to whole seconds (supports float/timedelta inputs)
|
|
107
|
+
if isinstance(interval, timedelta):
|
|
108
|
+
interval_sec = int(interval.total_seconds())
|
|
109
|
+
else:
|
|
110
|
+
interval_sec = int(interval)
|
|
111
|
+
|
|
112
|
+
if interval_sec <= 0:
|
|
113
|
+
raise ValueError("interval must be positive (seconds)")
|
|
114
|
+
|
|
115
|
+
# Accept ObsPy UTCDateTime transparently (preserve type on return)
|
|
116
|
+
_is_obspy = False
|
|
117
|
+
try:
|
|
118
|
+
from obspy import UTCDateTime as _UTCDateTime # type: ignore
|
|
119
|
+
if isinstance(dt, _UTCDateTime):
|
|
120
|
+
_is_obspy = True
|
|
121
|
+
dt_py = dt.datetime # Python datetime in UTC
|
|
122
|
+
else:
|
|
123
|
+
dt_py = dt
|
|
124
|
+
except Exception:
|
|
125
|
+
dt_py = dt
|
|
126
|
+
|
|
127
|
+
epoch = (
|
|
128
|
+
datetime(1970, 1, 1)
|
|
129
|
+
if dt_py.tzinfo is None
|
|
130
|
+
else datetime(1970, 1, 1, tzinfo=dt_py.tzinfo)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Compute integer seconds since epoch to avoid float precision issues
|
|
134
|
+
seconds = int((dt_py - epoch).total_seconds())
|
|
135
|
+
floored = (seconds + 0.5 * interval_sec) % interval_sec
|
|
136
|
+
rounded = epoch + timedelta(seconds=seconds + 0.5 * interval_sec - floored)
|
|
137
|
+
|
|
138
|
+
if _is_obspy:
|
|
139
|
+
from obspy import UTCDateTime as _UTCDateTime # type: ignore
|
|
140
|
+
return _UTCDateTime(rounded)
|
|
141
|
+
|
|
142
|
+
return rounded
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def floor_datetime(dt: datetime, interval: Union[int, float, timedelta]) -> datetime:
|
|
146
|
+
"""
|
|
147
|
+
Floor a datetime to the latest multiple of a given interval.
|
|
148
|
+
|
|
149
|
+
Assumes ``dt`` represents a UTC time (naive or tz-aware is fine) and
|
|
150
|
+
aligns against the Unix epoch 1970-01-01T00:00:00Z. The interval is in
|
|
151
|
+
seconds (int/float) or a timedelta. Returns a datetime with the same
|
|
152
|
+
"naive vs aware" form as ``dt``.
|
|
153
|
+
|
|
154
|
+
Examples
|
|
155
|
+
--------
|
|
156
|
+
>>> from datetime import datetime
|
|
157
|
+
>>> floor_datetime(datetime.fromisoformat('2025-11-27T10:12:43'), 600)
|
|
158
|
+
datetime.datetime(2025, 11, 27, 10, 10)
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
# Normalize interval to whole seconds (supports float/timedelta inputs)
|
|
162
|
+
if isinstance(interval, timedelta):
|
|
163
|
+
interval_sec = int(interval.total_seconds())
|
|
164
|
+
else:
|
|
165
|
+
interval_sec = int(interval)
|
|
166
|
+
|
|
167
|
+
if interval_sec <= 0:
|
|
168
|
+
raise ValueError("interval must be positive (seconds)")
|
|
169
|
+
|
|
170
|
+
# Accept ObsPy UTCDateTime transparently (preserve type on return)
|
|
171
|
+
_is_obspy = False
|
|
172
|
+
try:
|
|
173
|
+
from obspy import UTCDateTime as _UTCDateTime # type: ignore
|
|
174
|
+
if isinstance(dt, _UTCDateTime):
|
|
175
|
+
_is_obspy = True
|
|
176
|
+
dt_py = dt.datetime # Python datetime in UTC
|
|
177
|
+
else:
|
|
178
|
+
dt_py = dt
|
|
179
|
+
except Exception:
|
|
180
|
+
dt_py = dt
|
|
181
|
+
|
|
182
|
+
epoch = (
|
|
183
|
+
datetime(1970, 1, 1)
|
|
184
|
+
if dt_py.tzinfo is None
|
|
185
|
+
else datetime(1970, 1, 1, tzinfo=dt_py.tzinfo)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Compute integer seconds since epoch to avoid float precision issues
|
|
189
|
+
seconds = int((dt_py - epoch).total_seconds())
|
|
190
|
+
floored = seconds - (seconds % interval_sec)
|
|
191
|
+
rounded = epoch + timedelta(seconds=floored)
|
|
192
|
+
|
|
193
|
+
if _is_obspy:
|
|
194
|
+
from obspy import UTCDateTime as _UTCDateTime # type: ignore
|
|
195
|
+
return _UTCDateTime(rounded)
|
|
196
|
+
|
|
197
|
+
return rounded
|
|
198
|
+
|
|
199
|
+
|
|
62
200
|
def get_dt(times):
|
|
63
201
|
"""
|
|
64
202
|
Infer the sampling of the time dimension.
|
tonik/xarray2zarr.py
CHANGED
|
@@ -267,7 +267,7 @@ def xarray2zarr(xds: xr.Dataset, path: str, mode: str = 'a', group='original',
|
|
|
267
267
|
continue
|
|
268
268
|
|
|
269
269
|
if xds_existing[timedim][0] > xds[timedim][-1]:
|
|
270
|
-
|
|
270
|
+
logger.debug("Prepending data to existing zarr store.")
|
|
271
271
|
xda_new = fill_time_gaps_between_datasets(xds_existing[feature].isel({timedim: 0}),
|
|
272
272
|
xds[feature], mode='p')
|
|
273
273
|
xda_new = _build_append_payload_full_chunks(
|
|
@@ -277,7 +277,7 @@ def xarray2zarr(xds: xr.Dataset, path: str, mode: str = 'a', group='original',
|
|
|
277
277
|
write_empty_chunks=True)
|
|
278
278
|
|
|
279
279
|
elif xds_existing[timedim][-1] < xds[timedim][0]:
|
|
280
|
-
|
|
280
|
+
logger.debug("Appending data to existing zarr store.")
|
|
281
281
|
xda_new = fill_time_gaps_between_datasets(xds_existing[feature].isel({timedim: -1}),
|
|
282
282
|
xds[feature], mode='a')
|
|
283
283
|
xda_new = _build_append_payload_full_chunks(
|
|
@@ -286,13 +286,19 @@ def xarray2zarr(xds: xr.Dataset, path: str, mode: str = 'a', group='original',
|
|
|
286
286
|
append_dim=timedim)
|
|
287
287
|
|
|
288
288
|
elif xds_existing[timedim][0] > xds[timedim][0] and xds_existing[timedim][-1] < xds[timedim][-1]:
|
|
289
|
-
|
|
289
|
+
logger.debug(
|
|
290
|
+
"Data in zarr store contained in new data. Rewriting zarr store.")
|
|
290
291
|
xda_new = _build_append_payload_full_chunks(
|
|
291
292
|
xds[feature], 'a', nchunks)
|
|
292
293
|
xda_new.to_zarr(fout, group=group, mode='w',
|
|
293
294
|
write_empty_chunks=True)
|
|
294
295
|
|
|
295
296
|
else:
|
|
297
|
+
logger.debug("Data in zarr store overlaps with new data.")
|
|
298
|
+
logger.debug(
|
|
299
|
+
f"Endtime of existing data: {xds_existing[timedim][-1].values}")
|
|
300
|
+
logger.debug(f"Starttime of new data: {xds[timedim][0].values}")
|
|
301
|
+
xds_existing = xds_existing.drop_duplicates(timedim, keep='last')
|
|
296
302
|
overlap = xds_existing[timedim].where(
|
|
297
303
|
xds_existing[timedim] == xds[timedim])
|
|
298
304
|
xds[feature].loc[{timedim: overlap}].to_zarr(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tonik
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.21
|
|
4
4
|
Summary: Store time series data as HDF5 files and access them through an API.
|
|
5
5
|
Project-URL: Homepage, https://tsc-tools.github.io/tonik
|
|
6
6
|
Project-URL: Issues, https://github.com/tsc-tools/tonik/issues
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
tonik/__init__.py,sha256=dov-nMeGFBzLspmj4rWKjC4r736vmaPDgMEkHSUfP98,523
|
|
2
2
|
tonik/api.py,sha256=vW0ykOo5iGAV0_WuOepdrnUyFp83F7KyJTd43ksLmUk,7985
|
|
3
3
|
tonik/grafana_annotations.py,sha256=ZU9Cy-HT4vvMfYIQzD9WboaDVOCBDv__NmXbk1qKWJo,5838
|
|
4
|
-
tonik/storage.py,sha256=
|
|
5
|
-
tonik/utils.py,sha256=
|
|
4
|
+
tonik/storage.py,sha256=bYBl3JPpH8D3iIFOj5AZQXc4M8txAbwFKt4xTfdotgg,10583
|
|
5
|
+
tonik/utils.py,sha256=GwAXfGFQWhlsLThQvSux1SooRkW-iIkJP99JMH72t5Y,11791
|
|
6
6
|
tonik/xarray2netcdf.py,sha256=nq6RHk5ciaAg1bxNDiyHPRdAts1C7fj7jtDbaLaSTWM,6497
|
|
7
|
-
tonik/xarray2zarr.py,sha256=
|
|
7
|
+
tonik/xarray2zarr.py,sha256=kRhgDdo8CDT1ceszwQEQNfXdgbnmL5nNejUzaMnyFXM,11707
|
|
8
8
|
tonik/package_data/index.html,sha256=ZCZ-BtGRERsL-6c_dfY43qd2WAaggH7xereennGL6ww,4372
|
|
9
9
|
tonik/package_data/whakaari_labels.json,sha256=96UZSq41yXgAJxuKivLBKlRTw-33jkjh7AGKTsDQ9Yg,3993
|
|
10
|
-
tonik-0.1.
|
|
11
|
-
tonik-0.1.
|
|
12
|
-
tonik-0.1.
|
|
13
|
-
tonik-0.1.
|
|
14
|
-
tonik-0.1.
|
|
10
|
+
tonik-0.1.21.dist-info/METADATA,sha256=rEp5KTN5xDizNLajJkP3i6lvGI02hrR-sqqfIRfn4M0,2207
|
|
11
|
+
tonik-0.1.21.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
12
|
+
tonik-0.1.21.dist-info/entry_points.txt,sha256=y82XyTeQddM87gCTzgSQaTlKF3VFicO4hhClHUv6j1A,127
|
|
13
|
+
tonik-0.1.21.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
14
|
+
tonik-0.1.21.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|