tonik 0.1.9__py3-none-any.whl → 0.1.11__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/__init__.py +1 -1
- tonik/api.py +25 -4
- tonik/storage.py +29 -1
- tonik/utils.py +99 -1
- tonik/xarray2netcdf.py +1 -1
- {tonik-0.1.9.dist-info → tonik-0.1.11.dist-info}/METADATA +7 -10
- tonik-0.1.11.dist-info/RECORD +12 -0
- {tonik-0.1.9.dist-info → tonik-0.1.11.dist-info}/WHEEL +1 -1
- {tonik-0.1.9.dist-info → tonik-0.1.11.dist-info}/entry_points.txt +1 -0
- tonik-0.1.9.dist-info/RECORD +0 -12
- {tonik-0.1.9.dist-info → tonik-0.1.11.dist-info}/licenses/LICENSE +0 -0
tonik/__init__.py
CHANGED
tonik/api.py
CHANGED
|
@@ -10,7 +10,7 @@ import datashader as dsh
|
|
|
10
10
|
import numpy as np
|
|
11
11
|
import pandas as pd
|
|
12
12
|
import uvicorn
|
|
13
|
-
from cftime import date2num,
|
|
13
|
+
from cftime import date2num, num2pydate
|
|
14
14
|
from fastapi import FastAPI, HTTPException, Query
|
|
15
15
|
from fastapi.middleware.cors import CORSMiddleware
|
|
16
16
|
from fastapi.responses import HTMLResponse, StreamingResponse
|
|
@@ -37,6 +37,7 @@ class TonikAPI:
|
|
|
37
37
|
self.app.get("/", response_class=HTMLResponse)(self.root)
|
|
38
38
|
self.app.get("/feature")(self.feature)
|
|
39
39
|
self.app.get("/inventory")(self.inventory)
|
|
40
|
+
self.app.get("/labels")(self.labels)
|
|
40
41
|
|
|
41
42
|
def root(self):
|
|
42
43
|
with open(get_data("package_data/index.html"), "r", encoding="utf-8") as file:
|
|
@@ -101,11 +102,12 @@ class TonikAPI:
|
|
|
101
102
|
dates = np.tile(dates, freq.size)
|
|
102
103
|
df = pd.DataFrame(
|
|
103
104
|
{'dates': dates, 'freqs': freqs, 'feature': vals})
|
|
105
|
+
df.dates = pd.to_datetime(df.dates.values).tz_localize('UTC')
|
|
104
106
|
output = df.to_csv(index=False,
|
|
105
107
|
columns=['dates', 'freqs', 'feature'])
|
|
106
108
|
else:
|
|
107
109
|
df = pd.DataFrame(data=feat.to_pandas(), columns=[feat.name])
|
|
108
|
-
df['dates'] = df.index
|
|
110
|
+
df['dates'] = df.index.tz_localize('UTC')
|
|
109
111
|
if resolution != 'full':
|
|
110
112
|
try:
|
|
111
113
|
current_resolution = pd.Timedelta(
|
|
@@ -138,7 +140,7 @@ class TonikAPI:
|
|
|
138
140
|
agg = cvs.raster(source=feat)
|
|
139
141
|
freq_dim = feat.dims[0]
|
|
140
142
|
freq, d, spec = agg.coords[freq_dim].values, agg.coords['datetime'].values, agg.data
|
|
141
|
-
dates =
|
|
143
|
+
dates = num2pydate(
|
|
142
144
|
d, units='hours since 1970-01-01 00:00:00.0', calendar='gregorian')
|
|
143
145
|
return freq, dates, spec
|
|
144
146
|
|
|
@@ -156,15 +158,34 @@ class TonikAPI:
|
|
|
156
158
|
return sg.to_dict()
|
|
157
159
|
else:
|
|
158
160
|
dir_contents = os.listdir(c.path)
|
|
161
|
+
if 'labels.json' in dir_contents:
|
|
162
|
+
dir_contents.remove('labels.json')
|
|
159
163
|
return [fn.replace('.nc', '').replace('.zarr', '') for fn in dir_contents]
|
|
160
164
|
|
|
165
|
+
def labels(self, group: str, subdir: SubdirType = None, starttime: Optional[str] = None, endtime: Optional[str] = None):
|
|
166
|
+
_st = self.preprocess_datetime(starttime)
|
|
167
|
+
_et = self.preprocess_datetime(endtime)
|
|
168
|
+
sg = Storage(group, rootdir=self.rootdir,
|
|
169
|
+
starttime=_st, endtime=_et, create=False)
|
|
170
|
+
try:
|
|
171
|
+
c = sg.get_substore(*subdir)
|
|
172
|
+
except TypeError:
|
|
173
|
+
c = sg
|
|
174
|
+
except FileNotFoundError:
|
|
175
|
+
msg = "Directory {} not found.".format(
|
|
176
|
+
'/'.join([sg.path] + subdir))
|
|
177
|
+
raise HTTPException(status_code=404, detail=msg)
|
|
178
|
+
return c.get_labels()
|
|
179
|
+
|
|
161
180
|
|
|
162
181
|
def main(argv=None):
|
|
163
182
|
parser = ArgumentParser()
|
|
164
183
|
parser.add_argument("--rootdir", default='/tmp')
|
|
184
|
+
parser.add_argument("-p", "--port", default=8003, type=int)
|
|
185
|
+
parser.add_argument("--host", default='0.0.0.0')
|
|
165
186
|
args = parser.parse_args(argv)
|
|
166
187
|
ta = TonikAPI(args.rootdir)
|
|
167
|
-
uvicorn.run(ta.app, host=
|
|
188
|
+
uvicorn.run(ta.app, host=args.host, port=args.port)
|
|
168
189
|
|
|
169
190
|
|
|
170
191
|
if __name__ == "__main__":
|
tonik/storage.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import logging
|
|
2
3
|
import logging.config
|
|
3
4
|
import os
|
|
@@ -157,6 +158,30 @@ class Path(object):
|
|
|
157
158
|
with xr.open_dataset(filename, group='original', engine=self.engine) as ds:
|
|
158
159
|
return ds[feature].sizes
|
|
159
160
|
|
|
161
|
+
def save_labels(self, labels):
|
|
162
|
+
"""
|
|
163
|
+
Save all labels. Labels are stored in a list of dictionaries with the following keys:
|
|
164
|
+
{
|
|
165
|
+
'time': 'time, or the beginning of the time window',
|
|
166
|
+
'timeEnd': 'end of the time window [optional]',
|
|
167
|
+
'title': 'title of the label',
|
|
168
|
+
'text': 'A more detailed description of the label',
|
|
169
|
+
'tags': 'tags to sort labels',
|
|
170
|
+
'id': 'unique id of the label'
|
|
171
|
+
}
|
|
172
|
+
"""
|
|
173
|
+
filename = os.path.join(self.path, 'labels.json')
|
|
174
|
+
with open(filename, 'w') as f:
|
|
175
|
+
json.dump(labels, f)
|
|
176
|
+
|
|
177
|
+
def get_labels(self):
|
|
178
|
+
"""
|
|
179
|
+
Load all labels.
|
|
180
|
+
"""
|
|
181
|
+
filename = os.path.join(self.path, 'labels.json')
|
|
182
|
+
with open(filename) as f:
|
|
183
|
+
return json.load(f)
|
|
184
|
+
|
|
160
185
|
|
|
161
186
|
class Storage(Path):
|
|
162
187
|
"""
|
|
@@ -238,7 +263,10 @@ class Storage(Path):
|
|
|
238
263
|
if name.endswith('.zarr'):
|
|
239
264
|
return name.replace('.zarr', '')
|
|
240
265
|
elif os.path.isdir(path):
|
|
241
|
-
|
|
266
|
+
dir_contents = os.listdir(path)
|
|
267
|
+
if 'labels.json' in dir_contents:
|
|
268
|
+
dir_contents.remove('labels.json')
|
|
269
|
+
return {name: [Storage.directory_tree_to_dict(os.path.join(path, child)) for child in sorted(dir_contents)]}
|
|
242
270
|
else:
|
|
243
271
|
if name.endswith('.nc'):
|
|
244
272
|
return name.replace('.nc', '')
|
tonik/utils.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
from
|
|
1
|
+
from typing import List
|
|
2
|
+
from datetime import datetime, timezone, timedelta
|
|
2
3
|
|
|
3
4
|
import numpy as np
|
|
4
5
|
import pandas as pd
|
|
@@ -89,3 +90,100 @@ def merge_arrays(xds_old: xr.DataArray, xds_new: xr.DataArray,
|
|
|
89
90
|
freq=f'{resolution}h')
|
|
90
91
|
xda_new = xda_new.reindex(datetime=new_dates)
|
|
91
92
|
return xda_new
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def extract_consecutive_integers(nums: List[int]) -> List[List[int]]:
|
|
96
|
+
"""
|
|
97
|
+
Extract consecutive integers from a list of integers.
|
|
98
|
+
"""
|
|
99
|
+
if not len(nums) > 0:
|
|
100
|
+
return []
|
|
101
|
+
|
|
102
|
+
nums.sort() # Sort the array
|
|
103
|
+
result = []
|
|
104
|
+
temp = [nums[0]] # Initialize the first group with the first number
|
|
105
|
+
|
|
106
|
+
for i in range(1, len(nums)):
|
|
107
|
+
if nums[i] == nums[i - 1] + 1: # Check if consecutive
|
|
108
|
+
temp.append(nums[i])
|
|
109
|
+
else:
|
|
110
|
+
result.append(temp) # Add the current group to the result
|
|
111
|
+
temp = [nums[i]] # Start a new group
|
|
112
|
+
|
|
113
|
+
result.append(temp) # Add the last group
|
|
114
|
+
return result
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def get_labels(xda: xr.DataArray, threshold: float) -> dict:
|
|
118
|
+
"""
|
|
119
|
+
Generate labels for time windows where the values are exceeding the threshold.
|
|
120
|
+
"""
|
|
121
|
+
# add labels to time windows where the values are exceeding the 85th percentile
|
|
122
|
+
labels = {xda.name: []}
|
|
123
|
+
idx = np.where(xda > threshold)[0]
|
|
124
|
+
window_list = extract_consecutive_integers(idx)
|
|
125
|
+
ids = 0
|
|
126
|
+
for _w in window_list:
|
|
127
|
+
timeEnd = None
|
|
128
|
+
# Convert to unix time milliseconds
|
|
129
|
+
timeStart = xda.datetime.isel(dict(datetime=_w[0])).values.astype(
|
|
130
|
+
'datetime64[ms]').astype(int)
|
|
131
|
+
timeStart = int(timeStart)
|
|
132
|
+
if len(_w) > 1:
|
|
133
|
+
timeEnd = xda.datetime.isel(
|
|
134
|
+
dict(datetime=_w[-1])).values.astype('datetime64[ms]').astype(int)
|
|
135
|
+
timeEnd = int(timeEnd)
|
|
136
|
+
label = dict(time=timeStart,
|
|
137
|
+
timeEnd=timeEnd,
|
|
138
|
+
title='Greater 85th percentile',
|
|
139
|
+
description='Values exceed 85th percentile',
|
|
140
|
+
tags=['anomaly'],
|
|
141
|
+
id=ids)
|
|
142
|
+
ids += 1
|
|
143
|
+
labels[xda.name].append(label)
|
|
144
|
+
return labels
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def main():
|
|
148
|
+
from tonik import Storage
|
|
149
|
+
import logging
|
|
150
|
+
|
|
151
|
+
logger = logging.getLogger(__name__)
|
|
152
|
+
logger.info("Generating test data")
|
|
153
|
+
rootdir = '/tmp'
|
|
154
|
+
g = Storage('volcanoes', rootdir=rootdir)
|
|
155
|
+
st1 = g.get_substore('Mt Doom', 'MDR', '00', 'BHZ')
|
|
156
|
+
st2 = g.get_substore('Misty Mountain', 'MMS', '10', 'HHZ')
|
|
157
|
+
|
|
158
|
+
# Get start time of the current 10 minute window
|
|
159
|
+
tstart = datetime.now(timezone.utc).timestamp()
|
|
160
|
+
tstart -= tstart % 600
|
|
161
|
+
tstart = datetime.fromtimestamp(tstart, timezone.utc)
|
|
162
|
+
tstart -= timedelta(days=30)
|
|
163
|
+
tstart = tstart.replace(tzinfo=None)
|
|
164
|
+
logger.info(f"Start time: {tstart}")
|
|
165
|
+
|
|
166
|
+
# Generate test data
|
|
167
|
+
xdf_1D = generate_test_data(dim=1, tstart=tstart, add_nans=False, seed=42)
|
|
168
|
+
xdf_1D_1 = generate_test_data(
|
|
169
|
+
dim=1, tstart=tstart, add_nans=False, seed=24)
|
|
170
|
+
xdf_2D = generate_test_data(
|
|
171
|
+
dim=2, tstart=tstart, add_nans=False, seed=1234)
|
|
172
|
+
xdf_2D_1 = generate_test_data(
|
|
173
|
+
dim=2, tstart=tstart, add_nans=False, seed=4321)
|
|
174
|
+
logger.info("Saving test data to " + st1.path)
|
|
175
|
+
st1.save(xdf_1D)
|
|
176
|
+
st1.save(xdf_2D)
|
|
177
|
+
logger.info("Saving test data to " + st2.path)
|
|
178
|
+
st2.save(xdf_1D_1)
|
|
179
|
+
st2.save(xdf_2D_1)
|
|
180
|
+
|
|
181
|
+
# add labels to RSAM time windows where the values are exceeding the 85th percentile
|
|
182
|
+
logger.info("Adding labels to DSAR data")
|
|
183
|
+
labels = get_labels(xdf_1D.dsar, float(xdf_1D.dsar.quantile(0.85)))
|
|
184
|
+
logger.info("Saving labels to " + st1.path + '/labels.json')
|
|
185
|
+
st1.save_labels(labels)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
if __name__ == '__main__':
|
|
189
|
+
main()
|
tonik/xarray2netcdf.py
CHANGED
|
@@ -103,7 +103,7 @@ def _create_h5_Structure(defaultGroupName, featureName, h5f, xArray, starttime,
|
|
|
103
103
|
coordinates = rootGrp.create_variable(timedim, (timedim,), float)
|
|
104
104
|
coordinates.attrs['units'] = 'hours since 1970-01-01 00:00:00.0'
|
|
105
105
|
coordinates.attrs['calendar'] = 'gregorian'
|
|
106
|
-
rootGrp.attrs['
|
|
106
|
+
rootGrp.attrs['archive_starttime'] = str(starttime)
|
|
107
107
|
for label, size in xArray.sizes.items():
|
|
108
108
|
if not np.issubdtype(xArray[label].dtype, np.datetime64):
|
|
109
109
|
rootGrp.dimensions[label] = size
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: tonik
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.11
|
|
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
|
|
@@ -10,8 +10,9 @@ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
|
10
10
|
Classifier: Operating System :: OS Independent
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
12
|
Requires-Python: >=3.7
|
|
13
|
+
Requires-Dist: dask<=2024.10.0
|
|
13
14
|
Requires-Dist: datashader>=0.14
|
|
14
|
-
Requires-Dist: fastapi>=0.
|
|
15
|
+
Requires-Dist: fastapi>=0.112
|
|
15
16
|
Requires-Dist: h5netcdf>=1.1
|
|
16
17
|
Requires-Dist: h5py>=3.8
|
|
17
18
|
Requires-Dist: matplotlib
|
|
@@ -30,8 +31,8 @@ Description-Content-Type: text/markdown
|
|
|
30
31
|
|
|
31
32
|
# Tonik
|
|
32
33
|
|
|
33
|
-
Tonik provides you with a solution to store and retrieve scientific data as well as serving it through an API.
|
|
34
|
-
For visualisations, the API can serve large requests very quickly by downsampling the data to the requested resolution on demand.
|
|
34
|
+
Tonik provides you with a solution to store and retrieve scientific time-series data as well as serving it through an API.
|
|
35
|
+
For visualisations, the API can serve large requests very quickly by downsampling the data to the requested resolution on demand. The API was optimised to visualise time-series and data labels with [Grafana](https://grafana.com/oss/grafana/).
|
|
35
36
|
|
|
36
37
|
## Requirements
|
|
37
38
|
* h5py
|
|
@@ -54,10 +55,6 @@ pip install -U tonik
|
|
|
54
55
|
|
|
55
56
|
Learn more about tonik in its official [documentation](https://tsc-tools.github.io/tonik)
|
|
56
57
|
|
|
57
|
-
## Contributing
|
|
58
|
-
|
|
59
|
-
You can find information about contributing to tonik at our [Contributing page]
|
|
60
|
-
|
|
61
58
|
## Get in touch
|
|
62
59
|
|
|
63
|
-
Report bugs, suggest features, view the source code, and ask questions [on GitHub](https://github.com/tsc-tools/tonik).
|
|
60
|
+
Report bugs, suggest features, view the source code, and ask questions [on GitHub](https://github.com/tsc-tools/tonik/issues).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
tonik/__init__.py,sha256=dov-nMeGFBzLspmj4rWKjC4r736vmaPDgMEkHSUfP98,523
|
|
2
|
+
tonik/api.py,sha256=xUH-fr-xUwc_a21QYz11Dk3YjB2nRuCclmALvU64UJM,7592
|
|
3
|
+
tonik/storage.py,sha256=IklM_atZD4rebUsnXsUj5JldSHU2LqmuqME03PHp_UI,10441
|
|
4
|
+
tonik/utils.py,sha256=9eSVKIbs8TIZlJCz_-B7FrvOUQCQHO3K52v4Heus-uE,6135
|
|
5
|
+
tonik/xarray2netcdf.py,sha256=gDNT6nxnRbXPeRqZ3URW5oXY3Nfh3TCrfueE-eUrIoY,5181
|
|
6
|
+
tonik/xarray2zarr.py,sha256=xJjKcFZF0oz6gw47apuCiXFtW5HgWqnZgiIuEVQHhBI,2363
|
|
7
|
+
tonik/package_data/index.html,sha256=GKDClUhIam_fAYbNfzAolORhSCG3ae1wW3VjWCg4PMk,2732
|
|
8
|
+
tonik-0.1.11.dist-info/METADATA,sha256=xEhlRUeS79ZRxwL_-ksmf5do2OrOapKBealQRPUSFpE,2005
|
|
9
|
+
tonik-0.1.11.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
10
|
+
tonik-0.1.11.dist-info/entry_points.txt,sha256=mT3B4eBE8SHlAeMhFnZGor9-YkVtoWM1NVHVuypJ-uY,74
|
|
11
|
+
tonik-0.1.11.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
12
|
+
tonik-0.1.11.dist-info/RECORD,,
|
tonik-0.1.9.dist-info/RECORD
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
tonik/__init__.py,sha256=ZBVGh4dm_l9xwiBGb33O5QV9MfZeNiEd3DBDAm6DiHk,511
|
|
2
|
-
tonik/api.py,sha256=RhESsP5JI6GTdz_XwoVok-mprWUtkPxtTfW8suEak3Y,6572
|
|
3
|
-
tonik/storage.py,sha256=GNJ6w9VHOeTR_ZJMZ-Ipqe3nFK2I91fkHYwg1k9bEuo,9470
|
|
4
|
-
tonik/utils.py,sha256=3nSRU_GnV6arP4e63YHn4oEV8XbqzVAW8FCvQVIwGdg,2757
|
|
5
|
-
tonik/xarray2netcdf.py,sha256=RUsPwPDEOnUIMGt7_9F1VDyg83zu8x-Qs3jmZs3Dq0o,5173
|
|
6
|
-
tonik/xarray2zarr.py,sha256=xJjKcFZF0oz6gw47apuCiXFtW5HgWqnZgiIuEVQHhBI,2363
|
|
7
|
-
tonik/package_data/index.html,sha256=GKDClUhIam_fAYbNfzAolORhSCG3ae1wW3VjWCg4PMk,2732
|
|
8
|
-
tonik-0.1.9.dist-info/METADATA,sha256=vzVUzQ3ZGqEJiIUd3MGZnFnDJE_7dQxcbHTwi3NLEDc,1938
|
|
9
|
-
tonik-0.1.9.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
10
|
-
tonik-0.1.9.dist-info/entry_points.txt,sha256=VnGfC5qAzpntEHAb5pooUEpYABSgOfQoNhCEtLDJyf8,45
|
|
11
|
-
tonik-0.1.9.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
12
|
-
tonik-0.1.9.dist-info/RECORD,,
|
|
File without changes
|