tonik 0.1.19__tar.gz → 0.1.20__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.
- {tonik-0.1.19 → tonik-0.1.20}/PKG-INFO +1 -1
- {tonik-0.1.19 → tonik-0.1.20}/pixi.lock +2 -2
- {tonik-0.1.19 → tonik-0.1.20}/pyproject.toml +1 -1
- {tonik-0.1.19 → tonik-0.1.20}/src/tonik/utils.py +140 -2
- tonik-0.1.20/tests/test_utils.py +92 -0
- tonik-0.1.19/tests/test_utils.py +0 -11
- {tonik-0.1.19 → tonik-0.1.20}/.devcontainer/devcontainer.json +0 -0
- {tonik-0.1.19 → tonik-0.1.20}/.gitattributes +0 -0
- {tonik-0.1.19 → tonik-0.1.20}/.gitignore +0 -0
- {tonik-0.1.19 → tonik-0.1.20}/HOW_TO_RELEASE.md +0 -0
- {tonik-0.1.19 → tonik-0.1.20}/LICENSE +0 -0
- {tonik-0.1.19 → tonik-0.1.20}/README.md +0 -0
- {tonik-0.1.19 → tonik-0.1.20}/grafana_example/Dockerfile_api +0 -0
- {tonik-0.1.19 → tonik-0.1.20}/grafana_example/Dockerfile_grafana +0 -0
- {tonik-0.1.19 → tonik-0.1.20}/grafana_example/dashboards/demo_dashboard.json +0 -0
- {tonik-0.1.19 → tonik-0.1.20}/grafana_example/docker-compose.yml +0 -0
- {tonik-0.1.19 → tonik-0.1.20}/grafana_example/grafana.ini +0 -0
- {tonik-0.1.19 → tonik-0.1.20}/grafana_example/provisioning/dashboards/default.yaml +0 -0
- {tonik-0.1.19 → tonik-0.1.20}/grafana_example/provisioning/datasources/default.yaml +0 -0
- {tonik-0.1.19 → tonik-0.1.20}/mkdocs.yml +0 -0
- {tonik-0.1.19 → tonik-0.1.20}/pyproject.toml~ +0 -0
- {tonik-0.1.19 → tonik-0.1.20}/src/tonik/__init__.py +0 -0
- {tonik-0.1.19 → tonik-0.1.20}/src/tonik/api.py +0 -0
- {tonik-0.1.19 → tonik-0.1.20}/src/tonik/grafana_annotations.py +0 -0
- {tonik-0.1.19 → tonik-0.1.20}/src/tonik/package_data/index.html +0 -0
- {tonik-0.1.19 → tonik-0.1.20}/src/tonik/package_data/whakaari_labels.json +0 -0
- {tonik-0.1.19 → tonik-0.1.20}/src/tonik/storage.py +0 -0
- {tonik-0.1.19 → tonik-0.1.20}/src/tonik/xarray2netcdf.py +0 -0
- {tonik-0.1.19 → tonik-0.1.20}/src/tonik/xarray2zarr.py +0 -0
- {tonik-0.1.19 → tonik-0.1.20}/tests/backend_speed_test.py +0 -0
- {tonik-0.1.19 → tonik-0.1.20}/tests/conftest.py +0 -0
- {tonik-0.1.19 → tonik-0.1.20}/tests/test_api.py +0 -0
- {tonik-0.1.19 → tonik-0.1.20}/tests/test_save.py +0 -0
- {tonik-0.1.19 → tonik-0.1.20}/tests/test_storage.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tonik
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.20
|
|
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
|
|
@@ -3681,8 +3681,8 @@ packages:
|
|
|
3681
3681
|
timestamp: 1763054914403
|
|
3682
3682
|
- pypi: ./
|
|
3683
3683
|
name: tonik
|
|
3684
|
-
version: 0.1.
|
|
3685
|
-
sha256:
|
|
3684
|
+
version: 0.1.20
|
|
3685
|
+
sha256: 34ecbc02a23b22c1e089cf36f78c90c6c214a3e88b9d272f69e349157f1728c6
|
|
3686
3686
|
requires_dist:
|
|
3687
3687
|
- h5py>=3.8
|
|
3688
3688
|
- datashader>=0.14
|
|
@@ -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.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from datetime import datetime, timedelta, timezone
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from tonik.utils import (extract_consecutive_integers,
|
|
6
|
+
generate_test_data,
|
|
7
|
+
round_datetime,
|
|
8
|
+
floor_datetime)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_extract_consecutive_integers():
|
|
12
|
+
nums = [1, 2, 3, 5, 6, 7, 8, 10]
|
|
13
|
+
assert extract_consecutive_integers(
|
|
14
|
+
nums) == [[1, 2, 3], [5, 6, 7, 8], [10]]
|
|
15
|
+
assert extract_consecutive_integers([1]) == [[1]]
|
|
16
|
+
assert extract_consecutive_integers(np.array([1, 2, 4])) == [[1, 2], [4]]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_generate_test_data():
|
|
20
|
+
"""
|
|
21
|
+
Test data generation function.
|
|
22
|
+
"""
|
|
23
|
+
tstart = datetime.now(timezone.utc) - timedelta(days=30)
|
|
24
|
+
tstart = floor_datetime(tstart, timedelta(days=1))
|
|
25
|
+
tstart = tstart.replace(tzinfo=None)
|
|
26
|
+
data = generate_test_data(tstart='2023-01-01', freq='1min', seed=42,
|
|
27
|
+
ndays=3)
|
|
28
|
+
assert 'datetime' in data.coords
|
|
29
|
+
assert data.rsam.shape[0] == 3*24*60 # 24 hours + start point
|
|
30
|
+
assert 'rsam' in data.data_vars
|
|
31
|
+
assert 'dsar' in data.data_vars
|
|
32
|
+
# Check for NaNs
|
|
33
|
+
n_nans = np.isnan(data.dsar.values).sum()
|
|
34
|
+
assert n_nans == 408
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_floor_datetime_basic_10min():
|
|
38
|
+
dt = datetime.fromisoformat("2025-11-27T10:12:43")
|
|
39
|
+
out = floor_datetime(dt, 600)
|
|
40
|
+
assert out == datetime(2025, 11, 27, 10, 10, 0)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_floor_datetime_on_boundary():
|
|
44
|
+
dt = datetime.fromisoformat("2025-11-27T10:20:00")
|
|
45
|
+
out = floor_datetime(dt, 600)
|
|
46
|
+
assert out == dt
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_floor_datetime_timedelta_interval():
|
|
50
|
+
dt = datetime.fromisoformat("2025-11-27T10:29:59")
|
|
51
|
+
out = floor_datetime(dt, timedelta(minutes=10))
|
|
52
|
+
assert out == datetime(2025, 11, 27, 10, 20, 0)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_floor_datetime_invalid_interval():
|
|
56
|
+
dt = datetime.fromisoformat("2025-11-27T10:12:43")
|
|
57
|
+
with pytest.raises(ValueError):
|
|
58
|
+
floor_datetime(dt, 0)
|
|
59
|
+
with pytest.raises(ValueError):
|
|
60
|
+
floor_datetime(dt, -15)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_floor_datetime_preserves_timezone_utc():
|
|
64
|
+
dt = datetime(2025, 11, 27, 10, 12, 43, tzinfo=timezone.utc)
|
|
65
|
+
out = floor_datetime(dt, 600)
|
|
66
|
+
assert out == datetime(2025, 11, 27, 10, 10, 0, tzinfo=timezone.utc)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_floor_datetime_with_obspy_UTCDateTime():
|
|
70
|
+
try:
|
|
71
|
+
from obspy import UTCDateTime
|
|
72
|
+
except Exception:
|
|
73
|
+
pytest.skip("obspy not available")
|
|
74
|
+
|
|
75
|
+
t = UTCDateTime(2025, 11, 27, 10, 12, 43)
|
|
76
|
+
out = floor_datetime(t, 600)
|
|
77
|
+
assert isinstance(out, UTCDateTime)
|
|
78
|
+
assert out == UTCDateTime(2025, 11, 27, 10, 10, 0)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_round_datetime_basic_10min():
|
|
82
|
+
dt = datetime.fromisoformat("2025-11-27T10:12:43")
|
|
83
|
+
out = round_datetime(dt, 600)
|
|
84
|
+
assert out == datetime(2025, 11, 27, 10, 10)
|
|
85
|
+
|
|
86
|
+
dt = datetime.fromisoformat("2025-11-27T10:10:00")
|
|
87
|
+
out = round_datetime(dt, 600)
|
|
88
|
+
assert out == datetime(2025, 11, 27, 10, 10)
|
|
89
|
+
|
|
90
|
+
dt = datetime.fromisoformat("2025-11-27T10:17:00")
|
|
91
|
+
out = round_datetime(dt, 600)
|
|
92
|
+
assert out == datetime(2025, 11, 27, 10, 20)
|
tonik-0.1.19/tests/test_utils.py
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
|
|
3
|
-
from tonik.utils import extract_consecutive_integers
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def test_extract_consecutive_integers():
|
|
7
|
-
nums = [1, 2, 3, 5, 6, 7, 8, 10]
|
|
8
|
-
assert extract_consecutive_integers(
|
|
9
|
-
nums) == [[1, 2, 3], [5, 6, 7, 8], [10]]
|
|
10
|
-
assert extract_consecutive_integers([1]) == [[1]]
|
|
11
|
-
assert extract_consecutive_integers(np.array([1, 2, 4])) == [[1, 2], [4]]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|