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 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.jsonlogger.JsonFormatter", # The class to instantiate!
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 * 6 * 24
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
- # prepend
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
- # append
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
- # existing datetimes are contained in new array
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.19
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=jcCVx2N8J1ZBKM73k-OaxB0uxukn4VAM_-CCaCeAKwk,10589
5
- tonik/utils.py,sha256=vRFMoCU7dbfnnm5RALBR-XrpPGDFtQoeTDzxFiYf3bo,7522
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=Dg9_b6Zwj_UQMhOez6wrPeHn0rUffUHqbv-e4pP_t3w,11233
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.19.dist-info/METADATA,sha256=IdmsIGrgcA1LV2Ji59HlyeN9ykMT4n6iId6YcPO936Q,2207
11
- tonik-0.1.19.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
12
- tonik-0.1.19.dist-info/entry_points.txt,sha256=y82XyTeQddM87gCTzgSQaTlKF3VFicO4hhClHUv6j1A,127
13
- tonik-0.1.19.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
14
- tonik-0.1.19.dist-info/RECORD,,
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