volue-insight-timeseries 2.0.2__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.
- volue_insight_timeseries/VERSION +1 -0
- volue_insight_timeseries/__init__.py +11 -0
- volue_insight_timeseries/auth.py +71 -0
- volue_insight_timeseries/curves.py +1412 -0
- volue_insight_timeseries/events.py +127 -0
- volue_insight_timeseries/session.py +521 -0
- volue_insight_timeseries/util.py +377 -0
- volue_insight_timeseries-2.0.2.dist-info/METADATA +23 -0
- volue_insight_timeseries-2.0.2.dist-info/RECORD +12 -0
- volue_insight_timeseries-2.0.2.dist-info/WHEEL +5 -0
- volue_insight_timeseries-2.0.2.dist-info/licenses/LICENSE +21 -0
- volue_insight_timeseries-2.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Various utility and conversion functions to make it easier to work with
|
|
3
|
+
# the data from the backend
|
|
4
|
+
#
|
|
5
|
+
|
|
6
|
+
import calendar
|
|
7
|
+
import datetime
|
|
8
|
+
import dateutil.parser
|
|
9
|
+
import pandas as pd
|
|
10
|
+
import numpy as np
|
|
11
|
+
import warnings
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from urllib.parse import quote_plus
|
|
15
|
+
except ImportError:
|
|
16
|
+
from urllib import quote_plus
|
|
17
|
+
from zoneinfo import ZoneInfo
|
|
18
|
+
from zoneinfo._common import ZoneInfoNotFoundError
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Curve types
|
|
22
|
+
TIME_SERIES = "TIME_SERIES"
|
|
23
|
+
TAGGED = "TAGGED"
|
|
24
|
+
INSTANCES = "INSTANCES"
|
|
25
|
+
TAGGED_INSTANCES = "TAGGED_INSTANCES"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Frequency mapping from TS to Pandas
|
|
29
|
+
_TS_FREQ_TABLE = {
|
|
30
|
+
"Y": "YS",
|
|
31
|
+
"S": "2QS",
|
|
32
|
+
"Q": "QS",
|
|
33
|
+
"M": "MS",
|
|
34
|
+
"W": "W-MON",
|
|
35
|
+
"H12": "12h",
|
|
36
|
+
"H6": "6h",
|
|
37
|
+
"H3": "3h",
|
|
38
|
+
"H": "h",
|
|
39
|
+
"MIN30": "30min",
|
|
40
|
+
"MIN15": "15min",
|
|
41
|
+
"MIN5": "5min",
|
|
42
|
+
"MIN": "min",
|
|
43
|
+
"D": "D",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Mapping from various versions of Pandas to TS is built from map above,
|
|
47
|
+
# with some additions to support older versions of pandas
|
|
48
|
+
_PANDAS_FREQ_TABLE = {
|
|
49
|
+
"YS-JAN": "Y",
|
|
50
|
+
"AS-JAN": "Y",
|
|
51
|
+
"AS": "Y",
|
|
52
|
+
"2QS-JAN": "S",
|
|
53
|
+
"QS-JAN": "Q",
|
|
54
|
+
"12H": "H12",
|
|
55
|
+
"6H": "H6",
|
|
56
|
+
"3H": "H3",
|
|
57
|
+
"30T": "MIN30",
|
|
58
|
+
"15T": "MIN15",
|
|
59
|
+
"5T": "MIN5",
|
|
60
|
+
"T": "MIN",
|
|
61
|
+
}
|
|
62
|
+
for ts_freq, pandas_freq in _TS_FREQ_TABLE.items():
|
|
63
|
+
_PANDAS_FREQ_TABLE[pandas_freq.upper()] = ts_freq
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class CurveException(Exception):
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TS(object):
|
|
71
|
+
"""
|
|
72
|
+
A class to hold a basic time series.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self, id=None, name=None, frequency=None, time_zone=None, tag=None, issue_date=None, curve_type=None, points=None, input_dict=None
|
|
77
|
+
):
|
|
78
|
+
self.id = id
|
|
79
|
+
self.name = name
|
|
80
|
+
self.frequency = frequency
|
|
81
|
+
self.time_zone = time_zone
|
|
82
|
+
self.tag = tag
|
|
83
|
+
self.issue_date = issue_date
|
|
84
|
+
self.curve_type = curve_type
|
|
85
|
+
self.points = points
|
|
86
|
+
|
|
87
|
+
# input_dict is the json dict from the API
|
|
88
|
+
if input_dict is not None:
|
|
89
|
+
for k, v in input_dict.items():
|
|
90
|
+
setattr(self, k, v)
|
|
91
|
+
|
|
92
|
+
if self.time_zone is not None:
|
|
93
|
+
self.tz = parse_tz(self.time_zone)
|
|
94
|
+
else:
|
|
95
|
+
self.tz = ZoneInfo("CET")
|
|
96
|
+
|
|
97
|
+
if self.curve_type is None:
|
|
98
|
+
self.curve_type = detect_curve_type(self.issue_date, self.tag)
|
|
99
|
+
# Validation
|
|
100
|
+
if self.frequency is None:
|
|
101
|
+
raise CurveException("TS must have frequency")
|
|
102
|
+
|
|
103
|
+
def __str__(self):
|
|
104
|
+
size = ""
|
|
105
|
+
if self.points:
|
|
106
|
+
size = " size: {}".format(len(self.points))
|
|
107
|
+
return "TS: {}{}".format(self.fullname, size)
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def fullname(self):
|
|
111
|
+
attrs = []
|
|
112
|
+
if self.name:
|
|
113
|
+
attrs.append(self.name)
|
|
114
|
+
else:
|
|
115
|
+
if self.id:
|
|
116
|
+
attrs.append(str(self.id))
|
|
117
|
+
attrs.extend([self.curve_type, str(self.tz), self.frequency])
|
|
118
|
+
if self.tag:
|
|
119
|
+
attrs.append(self.tag)
|
|
120
|
+
if self.issue_date:
|
|
121
|
+
attrs.append(str(self.issue_date))
|
|
122
|
+
return " ".join(attrs)
|
|
123
|
+
|
|
124
|
+
def to_pandas(self, name=None):
|
|
125
|
+
"""Converting :class:`volue_insight_timeseries.util.TS` object
|
|
126
|
+
to a pandas.Series object
|
|
127
|
+
|
|
128
|
+
Parameters
|
|
129
|
+
----------
|
|
130
|
+
name: str, optional
|
|
131
|
+
Name of the returned pandas.Series object. If not given the name
|
|
132
|
+
of the curve will be used.
|
|
133
|
+
Returns
|
|
134
|
+
-------
|
|
135
|
+
pandas.Series
|
|
136
|
+
"""
|
|
137
|
+
if name is None:
|
|
138
|
+
name = self.fullname
|
|
139
|
+
if self.points is None or len(self.points) == 0:
|
|
140
|
+
return pd.Series(name=name, dtype="float64")
|
|
141
|
+
|
|
142
|
+
index = []
|
|
143
|
+
values = []
|
|
144
|
+
for row in self.points:
|
|
145
|
+
if len(row) != 2:
|
|
146
|
+
raise ValueError("Points have unexpected contents")
|
|
147
|
+
dt = datetime.datetime.fromtimestamp(row[0] / 1000.0, self.tz)
|
|
148
|
+
index.append(dt)
|
|
149
|
+
values.append(row[1])
|
|
150
|
+
res = pd.Series(name=name, index=index, data=values)
|
|
151
|
+
mapped_freq = res.asfreq(self._map_freq(self.frequency))
|
|
152
|
+
dropped = mapped_freq.dropna()
|
|
153
|
+
|
|
154
|
+
# Warn about edge case for Gas Day timezone during DST changes.
|
|
155
|
+
# Gas Day is a 24-hour period starting at 4:00 UTC in summer and 5:00 UTC in winter,
|
|
156
|
+
# and finishing at 4:00 UTC (or 5:00) the next day. corresponding to 6:00 local time in Germany year-round.
|
|
157
|
+
# A gas loader bug causes timestamps on the day after DST to shift to 5:00 or 7:00 instead of 6:00.
|
|
158
|
+
number_of_nan = sum(1 for point in self.points if point[1] is None)
|
|
159
|
+
if len(self.points) - number_of_nan != len(dropped):
|
|
160
|
+
warnings.warn(
|
|
161
|
+
f"Data length mismatch: original data length is {len(self.points)}, but mapped frequency data length is "
|
|
162
|
+
f"{len(dropped)}. This may indicate data truncation.",
|
|
163
|
+
RuntimeWarning,
|
|
164
|
+
stacklevel=2,
|
|
165
|
+
)
|
|
166
|
+
return mapped_freq
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
def from_pandas(pd_series):
|
|
170
|
+
# Clean up some of the more common Pandas/api problems
|
|
171
|
+
pd_series = pd_series.astype(np.float64)
|
|
172
|
+
pd_series.replace({np.nan: None}, inplace=True)
|
|
173
|
+
|
|
174
|
+
name = pd_series.name
|
|
175
|
+
frequency = TS._rev_map_freq(pd_series.index.freqstr)
|
|
176
|
+
|
|
177
|
+
points = []
|
|
178
|
+
for i in pd_series.index:
|
|
179
|
+
t = i.astimezone("UTC")
|
|
180
|
+
timestamp = int(calendar.timegm(t.timetuple()) * 1000)
|
|
181
|
+
points.append([timestamp, pd_series[i]])
|
|
182
|
+
|
|
183
|
+
if is_integer(name):
|
|
184
|
+
return TS(id=int(name), frequency=frequency, points=points)
|
|
185
|
+
else:
|
|
186
|
+
return TS(name=name, frequency=frequency, points=points)
|
|
187
|
+
|
|
188
|
+
@staticmethod
|
|
189
|
+
def _map_freq(frequency):
|
|
190
|
+
if frequency.upper() in _TS_FREQ_TABLE:
|
|
191
|
+
frequency = _TS_FREQ_TABLE[frequency.upper()]
|
|
192
|
+
return frequency
|
|
193
|
+
|
|
194
|
+
@staticmethod
|
|
195
|
+
def _rev_map_freq(frequency):
|
|
196
|
+
if frequency.upper() in _PANDAS_FREQ_TABLE:
|
|
197
|
+
frequency = _PANDAS_FREQ_TABLE[frequency.upper()]
|
|
198
|
+
else:
|
|
199
|
+
warnings.warn(f"Frequency is not supported: '{frequency}'", FutureWarning, stacklevel=2)
|
|
200
|
+
return frequency
|
|
201
|
+
|
|
202
|
+
@staticmethod
|
|
203
|
+
def sum(ts_list, name):
|
|
204
|
+
"""calculate the sum of a given list
|
|
205
|
+
of :class:`volue_insight_timeseries.util.TS` objects
|
|
206
|
+
|
|
207
|
+
Returns a :class:`~volue_insight_timeseries.util.TS`
|
|
208
|
+
(:class:`volue_insight_timeseries.util.TS`) object that is
|
|
209
|
+
the sum of a list of TS objects with the given name.
|
|
210
|
+
|
|
211
|
+
Parameters
|
|
212
|
+
----------
|
|
213
|
+
ts_list: list
|
|
214
|
+
list of TS objects
|
|
215
|
+
name: str
|
|
216
|
+
Name of the returned TS object.
|
|
217
|
+
Returns
|
|
218
|
+
-------
|
|
219
|
+
:class:`volue_insight_timeseries.util.TS` object
|
|
220
|
+
"""
|
|
221
|
+
df = _ts_list_to_dataframe(ts_list)
|
|
222
|
+
return _generated_series_to_TS(df.sum(axis=1), name)
|
|
223
|
+
|
|
224
|
+
@staticmethod
|
|
225
|
+
def mean(ts_list, name):
|
|
226
|
+
"""calculate the mean of a given list of TS objects
|
|
227
|
+
|
|
228
|
+
Returns a TS (:class:`volue_insight_timeseries.util.TS`) object that is
|
|
229
|
+
the mean of a list of TS objects with the given name.
|
|
230
|
+
|
|
231
|
+
Parameters
|
|
232
|
+
----------
|
|
233
|
+
ts_list: list
|
|
234
|
+
list of TS objects
|
|
235
|
+
name: str
|
|
236
|
+
Name of the returned TS object.
|
|
237
|
+
Returns
|
|
238
|
+
-------
|
|
239
|
+
:class:`volue_insight_timeseries.util.TS` object
|
|
240
|
+
"""
|
|
241
|
+
df = _ts_list_to_dataframe(ts_list)
|
|
242
|
+
return _generated_series_to_TS(df.mean(axis=1), name)
|
|
243
|
+
|
|
244
|
+
@staticmethod
|
|
245
|
+
def median(ts_list, name):
|
|
246
|
+
"""calculate the median of a given list of TS objects
|
|
247
|
+
|
|
248
|
+
Returns a TS (:class:`volue_insight_timeseries.util.TS`) object that is
|
|
249
|
+
the median of a list of TS objects with the given name.
|
|
250
|
+
|
|
251
|
+
Parameters
|
|
252
|
+
----------
|
|
253
|
+
ts_list: list
|
|
254
|
+
list of TS objects
|
|
255
|
+
name: str
|
|
256
|
+
Name of the returned TS object.
|
|
257
|
+
Returns
|
|
258
|
+
-------
|
|
259
|
+
:class:`volue_insight_timeseries.util.TS` object
|
|
260
|
+
"""
|
|
261
|
+
df = _ts_list_to_dataframe(ts_list)
|
|
262
|
+
return _generated_series_to_TS(df.median(axis=1), name)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _generated_series_to_TS(series, name):
|
|
266
|
+
series.name = name
|
|
267
|
+
return TS.from_pandas(series)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _ts_list_to_dataframe(ts_list):
|
|
271
|
+
pd_list = []
|
|
272
|
+
for ts in ts_list:
|
|
273
|
+
pd_list.append(ts.to_pandas())
|
|
274
|
+
|
|
275
|
+
return pd.concat(pd_list, axis=1)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def tags_to_DF(tagged_list):
|
|
279
|
+
"""
|
|
280
|
+
Given a list of tagged series/instances, create a DataFrame with the tag of
|
|
281
|
+
each as column name
|
|
282
|
+
"""
|
|
283
|
+
return pd.DataFrame({s.tag: s.to_pandas() for s in tagged_list})
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
#
|
|
287
|
+
# Some parsing helpers
|
|
288
|
+
#
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def parsetime(datestr, tz=None):
|
|
292
|
+
"""
|
|
293
|
+
Parse the input date and optionally convert to correct time zone
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
d = dateutil.parser.parse(datestr)
|
|
297
|
+
|
|
298
|
+
if tz is not None:
|
|
299
|
+
if not isinstance(tz, datetime.tzinfo):
|
|
300
|
+
tz = parse_tz(tz)
|
|
301
|
+
|
|
302
|
+
if d.tzinfo is not None:
|
|
303
|
+
d = d.astimezone(tz)
|
|
304
|
+
else:
|
|
305
|
+
d = d.replace(tzinfo=tz)
|
|
306
|
+
|
|
307
|
+
else:
|
|
308
|
+
# If datestr does not have tzinfo and no tz given, assume CET
|
|
309
|
+
if d.tzinfo is None:
|
|
310
|
+
d = d.replace(tzinfo=ZoneInfo("CET"))
|
|
311
|
+
return d
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def parserange(rangeobj, tz=None):
|
|
315
|
+
"""
|
|
316
|
+
Parse a range object (a pair of date strings, which may each be None)
|
|
317
|
+
"""
|
|
318
|
+
if rangeobj.get("empty") is True:
|
|
319
|
+
return None
|
|
320
|
+
begin = rangeobj.get("begin")
|
|
321
|
+
end = rangeobj.get("end")
|
|
322
|
+
if begin is not None:
|
|
323
|
+
begin = parsetime(begin, tz=tz)
|
|
324
|
+
if end is not None:
|
|
325
|
+
end = parsetime(end, tz=tz)
|
|
326
|
+
return (begin, end)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
_tzmap = {
|
|
330
|
+
"CEGT": "CET",
|
|
331
|
+
"WEGT": "WET",
|
|
332
|
+
"PST": "US/Pacific",
|
|
333
|
+
"TRT": "Turkey",
|
|
334
|
+
"MSK": "Europe/Moscow",
|
|
335
|
+
"ART": "America/Argentina/Buenos_Aires",
|
|
336
|
+
"JST": "Asia/Tokyo",
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def parse_tz(time_zone):
|
|
341
|
+
try:
|
|
342
|
+
if time_zone in _tzmap:
|
|
343
|
+
time_zone = _tzmap[time_zone]
|
|
344
|
+
return ZoneInfo(time_zone)
|
|
345
|
+
except ZoneInfoNotFoundError:
|
|
346
|
+
return ZoneInfo("CET")
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def detect_curve_type(issue_date, tag):
|
|
350
|
+
if issue_date is None and tag is None:
|
|
351
|
+
return TIME_SERIES
|
|
352
|
+
elif issue_date is None:
|
|
353
|
+
return TAGGED
|
|
354
|
+
elif tag is None:
|
|
355
|
+
return INSTANCES
|
|
356
|
+
else:
|
|
357
|
+
return TAGGED_INSTANCES
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def is_integer(s):
|
|
361
|
+
try:
|
|
362
|
+
int(s)
|
|
363
|
+
return True
|
|
364
|
+
except ValueError:
|
|
365
|
+
return False
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def make_arg(key, value):
|
|
369
|
+
if hasattr(value, "__iter__") and not isinstance(value, str):
|
|
370
|
+
return "&".join([make_arg(key, v) for v in value])
|
|
371
|
+
|
|
372
|
+
if isinstance(value, datetime.date):
|
|
373
|
+
tmp = value.isoformat()
|
|
374
|
+
else:
|
|
375
|
+
tmp = "{}".format(value)
|
|
376
|
+
v = quote_plus(tmp)
|
|
377
|
+
return "{}={}".format(key, v)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: volue_insight_timeseries
|
|
3
|
+
Version: 2.0.2
|
|
4
|
+
Summary: Volue Insight API python library
|
|
5
|
+
Home-page: https://www.volueinsight.com
|
|
6
|
+
Author: Volue Insight
|
|
7
|
+
Author-email: support.insight@volue.com
|
|
8
|
+
Requires-Python: >=3.10, <3.14
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: requests>=2.18
|
|
11
|
+
Requires-Dist: sseclient-py>=1.7
|
|
12
|
+
Requires-Dist: pandas>=1.5.0; python_version >= "3.10" and python_version < "3.13"
|
|
13
|
+
Requires-Dist: pandas>=2.3.2; python_version ~= "3.13"
|
|
14
|
+
Dynamic: author
|
|
15
|
+
Dynamic: author-email
|
|
16
|
+
Dynamic: description
|
|
17
|
+
Dynamic: home-page
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
Dynamic: requires-dist
|
|
20
|
+
Dynamic: requires-python
|
|
21
|
+
Dynamic: summary
|
|
22
|
+
|
|
23
|
+
This library is meant as a simple toolkit for working with data from https://api.volueinsight.com/ (or equivalent services). Note that access is based on some sort of login credentials, this library is not all that useful unless you have a valid Volue Insight account.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
volue_insight_timeseries/VERSION,sha256=tLFvrqUtwEzP4y97NCCUhVkj6_WZo8rBlARwotbXwmE,6
|
|
2
|
+
volue_insight_timeseries/__init__.py,sha256=DXR1tB3huHaG9wKRbpSEYhAat97msXrFgdp9eyYR5Wc,274
|
|
3
|
+
volue_insight_timeseries/auth.py,sha256=wetOsgVpAU3ApnDqjcjGsCbyWO1sewmByLPzvGSFiKg,2322
|
|
4
|
+
volue_insight_timeseries/curves.py,sha256=VN6EVuaLg-6QqIKLnCbGq0-vbe_rTd5FPu8irZoDxfo,65901
|
|
5
|
+
volue_insight_timeseries/events.py,sha256=GI_b6tMbmTZBmBiYXXddBb-7u9tEvbcdvREAitenvXs,4190
|
|
6
|
+
volue_insight_timeseries/session.py,sha256=Ml-6V_HcPD5v5xkiqWqhwcDlylgRH4GfTee_HS8JDHY,20281
|
|
7
|
+
volue_insight_timeseries/util.py,sha256=D5DstNtPXyrcLz7i5nNe4OqwKDNNbj57lQn67G_lPSo,10545
|
|
8
|
+
volue_insight_timeseries-2.0.2.dist-info/licenses/LICENSE,sha256=QW5nSxE0LozNpYWdyJm_0hco_PidycTnelwYzKdW7ZM,1069
|
|
9
|
+
volue_insight_timeseries-2.0.2.dist-info/METADATA,sha256=J1xazdqCvKTU6nR-MVG6ALiYp1xl6HBfbKvVhkTmiqs,902
|
|
10
|
+
volue_insight_timeseries-2.0.2.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
|
|
11
|
+
volue_insight_timeseries-2.0.2.dist-info/top_level.txt,sha256=IxOCIoqzMAV5gS4n6CIoRW9kBZnKQHyUPlDuJpEXmNY,25
|
|
12
|
+
volue_insight_timeseries-2.0.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2018 Wattsight AS
|
|
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 @@
|
|
|
1
|
+
volue_insight_timeseries
|