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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (79.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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