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,127 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
import sseclient
|
|
6
|
+
import threading
|
|
7
|
+
import queue
|
|
8
|
+
|
|
9
|
+
from . import curves, util
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EventListener:
|
|
13
|
+
def __init__(self, session, curve_list, start_time=None, timeout=None):
|
|
14
|
+
self.curve_cache = {}
|
|
15
|
+
ids = []
|
|
16
|
+
if not hasattr(curve_list, '__iter__') or isinstance(curve_list, str):
|
|
17
|
+
curve_list = [curve_list]
|
|
18
|
+
for curve in curve_list:
|
|
19
|
+
if isinstance(curve, curves.BaseCurve):
|
|
20
|
+
ids.append(curve.id)
|
|
21
|
+
self.curve_cache[curve.id] = curve
|
|
22
|
+
else:
|
|
23
|
+
ids.append(curve)
|
|
24
|
+
args = [util.make_arg('id', ids)]
|
|
25
|
+
if start_time is not None:
|
|
26
|
+
args.append(util.make_arg('start_time', start_time))
|
|
27
|
+
self.url = '/api/events?{}'.format('&'.join(args))
|
|
28
|
+
self.session = session
|
|
29
|
+
self.timeout = timeout
|
|
30
|
+
self.retry = 3000 # Retry time in milliseconds
|
|
31
|
+
self.client = None
|
|
32
|
+
self.queue = queue.Queue()
|
|
33
|
+
self.do_shutdown = False
|
|
34
|
+
self.worker = threading.Thread(target=self.fetch_events)
|
|
35
|
+
self.worker.daemon = True
|
|
36
|
+
self.worker.start()
|
|
37
|
+
|
|
38
|
+
def get(self):
|
|
39
|
+
try:
|
|
40
|
+
val = self.queue.get(timeout=self.timeout)
|
|
41
|
+
if isinstance(val, EventError):
|
|
42
|
+
raise val.exception
|
|
43
|
+
return val
|
|
44
|
+
except queue.Empty:
|
|
45
|
+
return EventTimeout()
|
|
46
|
+
|
|
47
|
+
def fetch_events(self):
|
|
48
|
+
while not self.do_shutdown:
|
|
49
|
+
try:
|
|
50
|
+
with self.session.data_request("GET", self.session.urlbase, self.url, stream=True) as stream:
|
|
51
|
+
self.client = sseclient.SSEClient(stream)
|
|
52
|
+
for sse_event in self.client.events():
|
|
53
|
+
if sse_event.event == 'curve_event':
|
|
54
|
+
event = CurveEvent(sse_event)
|
|
55
|
+
else:
|
|
56
|
+
event = DefaultEvent(sse_event)
|
|
57
|
+
if hasattr(event, 'id') and event.id in self.curve_cache:
|
|
58
|
+
event.curve = self.curve_cache[event.id]
|
|
59
|
+
self.queue.put(event)
|
|
60
|
+
if sse_event.retry is not None:
|
|
61
|
+
with contextlib.suppress(ValueError, TypeError):
|
|
62
|
+
self.retry = int(sse_event.retry)
|
|
63
|
+
if self.do_shutdown:
|
|
64
|
+
break
|
|
65
|
+
# Session was closed by server/network, wait for retry before looping.
|
|
66
|
+
time.sleep(self.retry / 1000.0)
|
|
67
|
+
except Exception as e:
|
|
68
|
+
self.queue.put(EventError(e))
|
|
69
|
+
break
|
|
70
|
+
|
|
71
|
+
def close(self, timeout=1):
|
|
72
|
+
self.do_shutdown = True
|
|
73
|
+
if self.client is not None:
|
|
74
|
+
self.client.close()
|
|
75
|
+
self.worker.join(timeout)
|
|
76
|
+
|
|
77
|
+
def __iter__(self):
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
def __next__(self):
|
|
81
|
+
return self.get()
|
|
82
|
+
|
|
83
|
+
def __enter__(self):
|
|
84
|
+
return self
|
|
85
|
+
|
|
86
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
87
|
+
self.close()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class EventError:
|
|
91
|
+
def __init__(self, exception):
|
|
92
|
+
self.exception = exception
|
|
93
|
+
|
|
94
|
+
def __str__(self):
|
|
95
|
+
return "{}".format(self.exception)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class EventTimeout:
|
|
99
|
+
"""Returned on timeout, etc."""
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class DefaultEvent(object):
|
|
104
|
+
def __init__(self, sse_event):
|
|
105
|
+
self._raw_event = sse_event
|
|
106
|
+
try:
|
|
107
|
+
self.json_data = json.loads(sse_event.data)
|
|
108
|
+
except (json.JSONDecodeError, TypeError):
|
|
109
|
+
self.json_data = None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class CurveEvent(DefaultEvent):
|
|
113
|
+
def __init__(self, sse_event):
|
|
114
|
+
super(CurveEvent, self).__init__(sse_event)
|
|
115
|
+
self.id = self.json_data['id']
|
|
116
|
+
self.curve = None
|
|
117
|
+
self.created = util.parsetime(self.json_data['created'])
|
|
118
|
+
self.operation = self.json_data['operation']
|
|
119
|
+
self.tag = None
|
|
120
|
+
self.issue_date = None
|
|
121
|
+
self.range = None
|
|
122
|
+
if 'tag' in self.json_data:
|
|
123
|
+
self.tag = self.json_data['tag']
|
|
124
|
+
if 'issue_date' in self.json_data:
|
|
125
|
+
self.issue_date = util.parsetime(self.json_data['issue_date'])
|
|
126
|
+
if 'range' in self.json_data:
|
|
127
|
+
self.range = util.parserange(self.json_data['range'])
|
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
try:
|
|
2
|
+
from urllib.parse import urljoin
|
|
3
|
+
except ImportError:
|
|
4
|
+
from urlparse import urljoin
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
import json
|
|
8
|
+
import time
|
|
9
|
+
import warnings
|
|
10
|
+
import configparser
|
|
11
|
+
|
|
12
|
+
from . import auth, curves, events, util
|
|
13
|
+
from .util import CurveException
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
RETRY_COUNT = 4 # Number of times to retry
|
|
17
|
+
RETRY_DELAY = 0.5 # Delay between retried calls, in seconds.
|
|
18
|
+
TIMEOUT = 300 # Default timeout for web calls, in seconds.
|
|
19
|
+
API_URLBASE = 'https://api.volueinsight.com'
|
|
20
|
+
AUTH_URLBASE = 'https://auth.volueinsight.com'
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ConfigException(Exception):
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MetadataException(Exception):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Session(object):
|
|
32
|
+
""" Establish a connection to Wattsight API
|
|
33
|
+
|
|
34
|
+
Creates an object that holds the state which is needed when talking to the
|
|
35
|
+
Wattsight data center. To establish a session, you have to provide
|
|
36
|
+
suthentication information either directly by using a ```client_id` and
|
|
37
|
+
``client_secret`` or using a ``config_file`` .
|
|
38
|
+
|
|
39
|
+
See https://api.volueinsight.com/#documentation for information how to get
|
|
40
|
+
your authentication data.
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
|
|
45
|
+
urlbase: url
|
|
46
|
+
Location of Wattsight service
|
|
47
|
+
config_file: path
|
|
48
|
+
path to the config.ini file which contains your authentication
|
|
49
|
+
information.
|
|
50
|
+
client_id: str
|
|
51
|
+
Your client ID
|
|
52
|
+
client_secret:
|
|
53
|
+
Your client secret.
|
|
54
|
+
auth_urlbase: url
|
|
55
|
+
Location of Wattsight authentication service
|
|
56
|
+
timeout: float
|
|
57
|
+
Timeout for REST calls, in seconds
|
|
58
|
+
|
|
59
|
+
Returns
|
|
60
|
+
-------
|
|
61
|
+
session: :class:`volue_insight_timeseries.session.Session` object
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, urlbase=None, config_file=None, client_id=None, client_secret=None,
|
|
66
|
+
auth_urlbase=None, timeout=None, retry_update_auth=False):
|
|
67
|
+
self.urlbase = API_URLBASE
|
|
68
|
+
self.auth = None
|
|
69
|
+
self.timeout = TIMEOUT
|
|
70
|
+
self._session = requests.Session()
|
|
71
|
+
self.retry_update_auth = retry_update_auth
|
|
72
|
+
if config_file is not None:
|
|
73
|
+
self.read_config_file(config_file)
|
|
74
|
+
elif client_id is not None and client_secret is not None:
|
|
75
|
+
self.configure(client_id, client_secret, auth_urlbase)
|
|
76
|
+
if urlbase is not None:
|
|
77
|
+
self.urlbase = urlbase
|
|
78
|
+
if timeout is not None:
|
|
79
|
+
self.timeout = timeout
|
|
80
|
+
|
|
81
|
+
def read_config_file(self, config_file):
|
|
82
|
+
"""Set up according to configuration file with hosts and access details"""
|
|
83
|
+
if self.auth is not None:
|
|
84
|
+
raise ConfigException('Session configuration is already done')
|
|
85
|
+
config = configparser.RawConfigParser()
|
|
86
|
+
# Support being given a file-like object or a file path:
|
|
87
|
+
if hasattr(config_file, 'read'):
|
|
88
|
+
config.read_file(config_file)
|
|
89
|
+
else:
|
|
90
|
+
files_read = config.read(config_file)
|
|
91
|
+
if not files_read:
|
|
92
|
+
raise ConfigException('Configuration file with name {} '
|
|
93
|
+
'was not found.'.format(config_file))
|
|
94
|
+
urlbase = config.get('common', 'urlbase', fallback=None)
|
|
95
|
+
if urlbase is not None:
|
|
96
|
+
self.urlbase = urlbase
|
|
97
|
+
auth_type = config.get('common', 'auth_type')
|
|
98
|
+
if auth_type == 'OAuth':
|
|
99
|
+
client_id = config.get(auth_type, 'id')
|
|
100
|
+
client_secret = config.get(auth_type, 'secret')
|
|
101
|
+
auth_urlbase = config.get(auth_type, 'auth_urlbase', fallback=AUTH_URLBASE)
|
|
102
|
+
self.auth = auth.OAuth(self, client_id, client_secret, auth_urlbase)
|
|
103
|
+
timeout = config.get('common', 'timeout', fallback=None)
|
|
104
|
+
if timeout is not None:
|
|
105
|
+
self.timeout = float(timeout)
|
|
106
|
+
|
|
107
|
+
def configure(self, client_id, client_secret, auth_urlbase=None):
|
|
108
|
+
"""Programmatically set authentication parameters"""
|
|
109
|
+
if self.auth is not None:
|
|
110
|
+
raise ConfigException('Session configuration is already done')
|
|
111
|
+
if auth_urlbase is None:
|
|
112
|
+
auth_urlbase = AUTH_URLBASE
|
|
113
|
+
self.auth = auth.OAuth(self, client_id, client_secret, auth_urlbase)
|
|
114
|
+
|
|
115
|
+
def get_curve(self, id=None, name=None):
|
|
116
|
+
"""Getting a curve object
|
|
117
|
+
|
|
118
|
+
Return a curve object of the correct type. Name should be specified.
|
|
119
|
+
While it is possible to get a curve by id, this is not guaranteed to be
|
|
120
|
+
long-term stable and will be removed in future versions.
|
|
121
|
+
|
|
122
|
+
Parameters
|
|
123
|
+
----------
|
|
124
|
+
|
|
125
|
+
id: int
|
|
126
|
+
curve id (deprecated)
|
|
127
|
+
name: str
|
|
128
|
+
curve name
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
curve object
|
|
133
|
+
Curve objects, can be one of:
|
|
134
|
+
:class:`~volue_insight_timeseries.curves.TimeSeriesCurve`,
|
|
135
|
+
:class:`~volue_insight_timeseries.curves.TaggedCurve`,
|
|
136
|
+
:class:`~volue_insight_timeseries.curves.InstanceCurve`,
|
|
137
|
+
:class:`~volue_insight_timeseries.curves.TaggedInstanceCurve`.
|
|
138
|
+
"""
|
|
139
|
+
if id is not None:
|
|
140
|
+
warnings.warn("Looking up a curve by ID will be removed in the future.", FutureWarning, stacklevel=2)
|
|
141
|
+
if id is None and name is None:
|
|
142
|
+
raise MetadataException('No curve specified')
|
|
143
|
+
|
|
144
|
+
if id is not None:
|
|
145
|
+
arg = util.make_arg('id', id)
|
|
146
|
+
else:
|
|
147
|
+
arg = util.make_arg('name', name)
|
|
148
|
+
response = self.data_request('GET', self.urlbase, '/api/curves/get?{}'.format(arg))
|
|
149
|
+
return self.handle_single_curve_response(response)
|
|
150
|
+
|
|
151
|
+
def search(self, query=None, id=None, name=None, commodity=None, category=None, area=None, station=None,
|
|
152
|
+
source=None, scenario=None, unit=None, time_zone=None, version=None, frequency=None, data_type=None,
|
|
153
|
+
curve_state=None, modified_since=None, only_accessible=None):
|
|
154
|
+
"""
|
|
155
|
+
Search for a curve matching various metadata.
|
|
156
|
+
|
|
157
|
+
This function searches for curves that matches the given search
|
|
158
|
+
parameters and returns a list of 0 or more curve objects.
|
|
159
|
+
A curve object can be a
|
|
160
|
+
:class:`~volue_insight_timeseries.curves.TimeSeriesCurve`,
|
|
161
|
+
:class:`~volue_insight_timeseries.curves.TaggedCurve`,
|
|
162
|
+
:class:`~volue_insight_timeseries.curves.InstanceCurve` or a
|
|
163
|
+
:class:`~volue_insight_timeseries.curves.TaggedInstanceCurve` object.
|
|
164
|
+
|
|
165
|
+
The search will return those curves matching all supplied parameters
|
|
166
|
+
(logical AND). For most parameters, a list of values may be supplied.
|
|
167
|
+
The search will match any of these values (logical OR). If a single
|
|
168
|
+
value contains a string with comma-separated values, these will be
|
|
169
|
+
treated as a list but will match with logical AND. (This only makes
|
|
170
|
+
sense for parameters where a curve may have multiple values:
|
|
171
|
+
area (border curves), category, source and scenario.)
|
|
172
|
+
|
|
173
|
+
For more details, see the REST documentation.
|
|
174
|
+
|
|
175
|
+
Parameters
|
|
176
|
+
----------
|
|
177
|
+
|
|
178
|
+
query: str
|
|
179
|
+
A query string used for a language-aware text search on both names
|
|
180
|
+
and descriptions of the various attributes in the curve.
|
|
181
|
+
|
|
182
|
+
id: int or lits of int
|
|
183
|
+
search for one or more specific id's (deprecated)
|
|
184
|
+
|
|
185
|
+
name: str or list of str
|
|
186
|
+
search for one or more curve names, you can use the ``*`` as
|
|
187
|
+
a wildcard for patter matching.
|
|
188
|
+
|
|
189
|
+
commodity: str or list of str
|
|
190
|
+
search for curves that match the given ``commodity`` attribute.
|
|
191
|
+
Get valid values for this attribute with
|
|
192
|
+
:meth:`volue_insight_timeseries.session.Session.get_commodities`
|
|
193
|
+
|
|
194
|
+
category: str or list of str
|
|
195
|
+
search for curves that match the given ``category`` attribute.
|
|
196
|
+
Get valid values for this attribute with
|
|
197
|
+
:meth:`volue_insight_timeseries.session.Session.get_categories`
|
|
198
|
+
|
|
199
|
+
area: str or list of str
|
|
200
|
+
search for curves that match the given ``area`` attribute.
|
|
201
|
+
Get valid values for this attribute with
|
|
202
|
+
:meth:`volue_insight_timeseries.session.Session.get_areas`
|
|
203
|
+
|
|
204
|
+
station: str or list of str
|
|
205
|
+
search for curves that match the given ``station`` attribute.
|
|
206
|
+
Get valid values for this attribute with
|
|
207
|
+
:meth:`volue_insight_timeseries.session.Session.get_stations`
|
|
208
|
+
|
|
209
|
+
source: str or list of str
|
|
210
|
+
search for curves that match the given ``source`` attribute.
|
|
211
|
+
Get valid values for this attribute with
|
|
212
|
+
:meth:`volue_insight_timeseries.session.Session.get_sources`
|
|
213
|
+
|
|
214
|
+
scenario: str or list of str
|
|
215
|
+
search for curves that match the given ``scenario`` attribute.
|
|
216
|
+
Get valid values for this attribute with
|
|
217
|
+
:meth:`volue_insight_timeseries.session.Session.get_scenarios`
|
|
218
|
+
|
|
219
|
+
unit: str or list of str
|
|
220
|
+
search for curves that match the given ``unit`` attribute.
|
|
221
|
+
Get valid values for this attribute with
|
|
222
|
+
:meth:`volue_insight_timeseries.session.Session.get_units`
|
|
223
|
+
|
|
224
|
+
time_zone: str or list of str
|
|
225
|
+
search for curves that match the given ``time_zone`` attribute.
|
|
226
|
+
Get valid values for this attribute with
|
|
227
|
+
:meth:`volue_insight_timeseries.session.Session.get_time_zones`
|
|
228
|
+
|
|
229
|
+
version: str or list of str
|
|
230
|
+
search for curves that match the given ``version`` attribute.
|
|
231
|
+
Get valid values for this attribute with
|
|
232
|
+
:meth:`volue_insight_timeseries.session.Session.get_versions`
|
|
233
|
+
|
|
234
|
+
frequency: str or list of str
|
|
235
|
+
search for curves that match the given ``frequency`` attribute.
|
|
236
|
+
Get valid values for this attribute with
|
|
237
|
+
:meth:`volue_insight_timeseries.session.Session.get_frequencies`
|
|
238
|
+
|
|
239
|
+
data_type: str or list of str
|
|
240
|
+
search for curves that match the given ``data_type`` attribute.
|
|
241
|
+
Get valid values for this attribute with
|
|
242
|
+
:meth:`volue_insight_timeseries.session.Session.get_data_types`
|
|
243
|
+
|
|
244
|
+
curve_state: str or list of str
|
|
245
|
+
search for curves that match the given ``curve_state`` attribute.
|
|
246
|
+
Get valid values for this attribute with
|
|
247
|
+
:meth:`volue_insight_timeseries.session.Session.get_curve_state`
|
|
248
|
+
|
|
249
|
+
modified_since: datestring, pandas.Timestamp or datetime.datetime
|
|
250
|
+
only return curves that where modified after given datetime.
|
|
251
|
+
|
|
252
|
+
only_accessible: bool
|
|
253
|
+
If True, only return curves you have (some) access to.
|
|
254
|
+
|
|
255
|
+
Returns
|
|
256
|
+
-------
|
|
257
|
+
curves: list
|
|
258
|
+
list of curve objects, can be one of:
|
|
259
|
+
:class:`~volue_insight_timeseries.curves.TimeSeriesCurve`,
|
|
260
|
+
:class:`~volue_insight_timeseries.curves.TaggedCurve`,
|
|
261
|
+
:class:`~volue_insight_timeseries.curves.InstanceCurve`,
|
|
262
|
+
:class:`~volue_insight_timeseries.curves.TaggedInstanceCurve`.
|
|
263
|
+
"""
|
|
264
|
+
search_terms = {
|
|
265
|
+
'query': query,
|
|
266
|
+
'id': id,
|
|
267
|
+
'name': name,
|
|
268
|
+
'commodity': commodity,
|
|
269
|
+
'category': category,
|
|
270
|
+
'area': area,
|
|
271
|
+
'station': station,
|
|
272
|
+
'source': source,
|
|
273
|
+
'scenario': scenario,
|
|
274
|
+
'unit': unit,
|
|
275
|
+
'time_zone': time_zone,
|
|
276
|
+
'version': version,
|
|
277
|
+
'frequency': frequency,
|
|
278
|
+
'data_type': data_type,
|
|
279
|
+
'curve_state': curve_state,
|
|
280
|
+
'modified_since': modified_since,
|
|
281
|
+
'only_accessible': only_accessible,
|
|
282
|
+
}
|
|
283
|
+
if id is not None:
|
|
284
|
+
warnings.warn("Searching for curves by ID will be removed in the future.", FutureWarning, stacklevel=2)
|
|
285
|
+
args = []
|
|
286
|
+
astr = ''
|
|
287
|
+
for key, val in search_terms.items():
|
|
288
|
+
if val is None:
|
|
289
|
+
continue
|
|
290
|
+
args.append(util.make_arg(key, val))
|
|
291
|
+
if len(args):
|
|
292
|
+
astr = "?{}".format("&".join(args))
|
|
293
|
+
# Now run the search, and try to produce a list of curves
|
|
294
|
+
response = self.data_request('GET', self.urlbase, '/api/curves{}'.format(astr))
|
|
295
|
+
return self.handle_multi_curve_response(response)
|
|
296
|
+
|
|
297
|
+
def make_curve(self, id, curve_type):
|
|
298
|
+
"""Return a mostly uninitialized curve object of the correct type.
|
|
299
|
+
This is generally a bad idea, use get_curve or search when possible."""
|
|
300
|
+
if curve_type in self._curve_types:
|
|
301
|
+
return self._curve_types[curve_type](id, None, self)
|
|
302
|
+
raise CurveException('Bad curve type requested')
|
|
303
|
+
|
|
304
|
+
def events(self, curve_list, start_time=None, timeout=None):
|
|
305
|
+
"""Get an event listener for a list of curves."""
|
|
306
|
+
return events.EventListener(self, curve_list, start_time=start_time, timeout=timeout)
|
|
307
|
+
|
|
308
|
+
_attributes = {'commodities', 'categories', 'areas', 'stations', 'sources', 'scenarios',
|
|
309
|
+
'units', 'time_zones', 'versions', 'frequencies', 'data_types',
|
|
310
|
+
'curve_states', 'curve_types', 'functions', 'filters'}
|
|
311
|
+
|
|
312
|
+
def get_commodities(self):
|
|
313
|
+
"""
|
|
314
|
+
Get valid values for the commodity attribute
|
|
315
|
+
"""
|
|
316
|
+
return self.get_attribute('commodities')
|
|
317
|
+
|
|
318
|
+
def get_categories(self):
|
|
319
|
+
"""
|
|
320
|
+
Get valid values for the category attribute
|
|
321
|
+
"""
|
|
322
|
+
return self.get_attribute('categories')
|
|
323
|
+
|
|
324
|
+
def get_areas(self):
|
|
325
|
+
"""
|
|
326
|
+
Get valid values for the area attribute
|
|
327
|
+
"""
|
|
328
|
+
return self.get_attribute('areas')
|
|
329
|
+
|
|
330
|
+
def get_stations(self):
|
|
331
|
+
"""
|
|
332
|
+
Get valid values for the station attribute
|
|
333
|
+
"""
|
|
334
|
+
return self.get_attribute('stations')
|
|
335
|
+
|
|
336
|
+
def get_sources(self):
|
|
337
|
+
"""
|
|
338
|
+
Get valid values for the source attribute
|
|
339
|
+
"""
|
|
340
|
+
return self.get_attribute('sources')
|
|
341
|
+
|
|
342
|
+
def get_scenarios(self):
|
|
343
|
+
"""
|
|
344
|
+
Get valid values for the scenarios attribute
|
|
345
|
+
"""
|
|
346
|
+
return self.get_attribute('scenarios')
|
|
347
|
+
|
|
348
|
+
def get_units(self):
|
|
349
|
+
"""
|
|
350
|
+
Get valid values for the unit attribute
|
|
351
|
+
"""
|
|
352
|
+
return self.get_attribute('units')
|
|
353
|
+
|
|
354
|
+
def get_time_zones(self):
|
|
355
|
+
"""
|
|
356
|
+
Get valid values for the time zone attribute
|
|
357
|
+
"""
|
|
358
|
+
return self.get_attribute('time_zones')
|
|
359
|
+
|
|
360
|
+
def get_versions(self):
|
|
361
|
+
"""
|
|
362
|
+
Get valid values for the version attribute
|
|
363
|
+
"""
|
|
364
|
+
return self.get_attribute('versions')
|
|
365
|
+
|
|
366
|
+
def get_frequencies(self):
|
|
367
|
+
"""
|
|
368
|
+
Get valid values for the frequency attribute
|
|
369
|
+
"""
|
|
370
|
+
return self.get_attribute('frequencies')
|
|
371
|
+
|
|
372
|
+
def get_data_types(self):
|
|
373
|
+
"""
|
|
374
|
+
Get valid values for the data_type attribute
|
|
375
|
+
"""
|
|
376
|
+
return self.get_attribute('data_types')
|
|
377
|
+
|
|
378
|
+
def get_curve_states(self):
|
|
379
|
+
"""
|
|
380
|
+
Get valid values for the curve_state attribute
|
|
381
|
+
"""
|
|
382
|
+
return self.get_attribute('curve_states')
|
|
383
|
+
|
|
384
|
+
def get_curve_types(self):
|
|
385
|
+
"""
|
|
386
|
+
Get valid values for the curve_type attribute
|
|
387
|
+
"""
|
|
388
|
+
return self.get_attribute('curve_types')
|
|
389
|
+
|
|
390
|
+
def get_functions(self):
|
|
391
|
+
"""
|
|
392
|
+
Get valid values for the function attribute
|
|
393
|
+
"""
|
|
394
|
+
return self.get_attribute('functions')
|
|
395
|
+
|
|
396
|
+
def get_filters(self):
|
|
397
|
+
"""
|
|
398
|
+
Get valid values for the filter attribute
|
|
399
|
+
"""
|
|
400
|
+
return self.get_attribute('filters')
|
|
401
|
+
|
|
402
|
+
def get_attribute(self, attribute):
|
|
403
|
+
"""Get valid values for an attribute."""
|
|
404
|
+
if attribute not in self._attributes:
|
|
405
|
+
raise MetadataException('Attribute {} is not valid'.format(attribute))
|
|
406
|
+
response = self.data_request('GET', self.urlbase, '/api/{}'.format(attribute))
|
|
407
|
+
if response.status_code == 200:
|
|
408
|
+
return response.json()
|
|
409
|
+
elif response.status_code == 204:
|
|
410
|
+
return None
|
|
411
|
+
raise MetadataException('Failed loading {}: {}'.format(attribute,
|
|
412
|
+
response.content.decode()))
|
|
413
|
+
|
|
414
|
+
_curve_types = {
|
|
415
|
+
util.TIME_SERIES: curves.TimeSeriesCurve,
|
|
416
|
+
util.TAGGED: curves.TaggedCurve,
|
|
417
|
+
util.INSTANCES: curves.InstanceCurve,
|
|
418
|
+
util.TAGGED_INSTANCES: curves.TaggedInstanceCurve,
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
_meta_keys = ('id', 'name', 'frequency', 'time_zone', 'curve_type')
|
|
422
|
+
|
|
423
|
+
def _build_curve(self, metadata):
|
|
424
|
+
for key in self._meta_keys:
|
|
425
|
+
if key not in metadata:
|
|
426
|
+
raise MetadataException('Mandatory key {} not found in metadata'.format(key))
|
|
427
|
+
curve_id = int(metadata['id'])
|
|
428
|
+
if('curve_state' in metadata and metadata['curve_state'] == 'DEPRECATED'):
|
|
429
|
+
warnings.warn("Deprecation warning for curve: {}".format(metadata['name']), DeprecationWarning, stacklevel=4)
|
|
430
|
+
if metadata['curve_type'] in self._curve_types:
|
|
431
|
+
c = self._curve_types[metadata['curve_type']](curve_id, metadata, self)
|
|
432
|
+
return c
|
|
433
|
+
raise CurveException('Unknown curve type ({})'.format(metadata['curve_type']))
|
|
434
|
+
|
|
435
|
+
def _get_auth_header_with_retry(self, databytes, retries=RETRY_COUNT):
|
|
436
|
+
try:
|
|
437
|
+
self.auth.validate_auth()
|
|
438
|
+
return self.auth.get_headers(databytes)
|
|
439
|
+
except Exception as e:
|
|
440
|
+
if retries <= 0:
|
|
441
|
+
raise e
|
|
442
|
+
if RETRY_DELAY > 0:
|
|
443
|
+
time.sleep(RETRY_DELAY)
|
|
444
|
+
return self._get_auth_header_with_retry(databytes, retries - 1)
|
|
445
|
+
|
|
446
|
+
def _validate_auth(self, data, rawdata):
|
|
447
|
+
headers = {}
|
|
448
|
+
|
|
449
|
+
databytes = None
|
|
450
|
+
if data is not None:
|
|
451
|
+
headers['content-type'] = 'application/json'
|
|
452
|
+
if isinstance(data, str):
|
|
453
|
+
databytes = data.encode()
|
|
454
|
+
else:
|
|
455
|
+
databytes = json.dumps(data).encode()
|
|
456
|
+
if data is None and rawdata is not None:
|
|
457
|
+
databytes = rawdata
|
|
458
|
+
if self.auth is not None:
|
|
459
|
+
# Beta-feature: Only update auth with retry if explicitly requested
|
|
460
|
+
if self.retry_update_auth:
|
|
461
|
+
auth_header = self._get_auth_header_with_retry(databytes)
|
|
462
|
+
headers.update(auth_header)
|
|
463
|
+
else:
|
|
464
|
+
self.auth.validate_auth()
|
|
465
|
+
headers.update(self.auth.get_headers(databytes))
|
|
466
|
+
|
|
467
|
+
return headers
|
|
468
|
+
|
|
469
|
+
def send_data_request(self, req_type, urlbase, url, data=None, rawdata=None, headers=None, authval=None,
|
|
470
|
+
stream=False, retries=RETRY_COUNT):
|
|
471
|
+
if not urlbase:
|
|
472
|
+
urlbase = self.urlbase
|
|
473
|
+
longurl = urljoin(urlbase, url)
|
|
474
|
+
|
|
475
|
+
databytes = None
|
|
476
|
+
if data is not None:
|
|
477
|
+
if isinstance(data, str):
|
|
478
|
+
databytes = data.encode()
|
|
479
|
+
else:
|
|
480
|
+
databytes = json.dumps(data).encode()
|
|
481
|
+
if data is None and rawdata is not None:
|
|
482
|
+
databytes = rawdata
|
|
483
|
+
timeout = None
|
|
484
|
+
try:
|
|
485
|
+
res = self._session.request(method=req_type, url=longurl, data=databytes,
|
|
486
|
+
headers=headers, auth=authval, stream=stream, timeout=self.timeout)
|
|
487
|
+
except requests.exceptions.Timeout as e:
|
|
488
|
+
timeout = e
|
|
489
|
+
res = None
|
|
490
|
+
if (timeout is not None or (500 <= res.status_code < 600) or res.status_code == 408) and retries > 0:
|
|
491
|
+
if RETRY_DELAY > 0:
|
|
492
|
+
time.sleep(RETRY_DELAY)
|
|
493
|
+
return self.send_data_request(req_type, urlbase, url, data, rawdata, headers, authval, stream, retries-1)
|
|
494
|
+
if timeout is not None:
|
|
495
|
+
raise timeout
|
|
496
|
+
return res
|
|
497
|
+
|
|
498
|
+
def data_request(self, req_type, urlbase, url, data=None, rawdata=None, authval=None,
|
|
499
|
+
stream=False, retries=RETRY_COUNT):
|
|
500
|
+
"""Run a call to the backend, dealing with authentication etc."""
|
|
501
|
+
headers = self._validate_auth(data, rawdata)
|
|
502
|
+
res = self.send_data_request(req_type, urlbase, url, data, rawdata, headers, authval, stream, retries)
|
|
503
|
+
return res
|
|
504
|
+
|
|
505
|
+
def handle_single_curve_response(self, response):
|
|
506
|
+
if not response.ok:
|
|
507
|
+
raise MetadataException('Failed to load curve: {}'
|
|
508
|
+
.format(response.content.decode()))
|
|
509
|
+
metadata = response.json()
|
|
510
|
+
return self._build_curve(metadata)
|
|
511
|
+
|
|
512
|
+
def handle_multi_curve_response(self, response):
|
|
513
|
+
if not response.ok:
|
|
514
|
+
raise MetadataException('Curve search failed: {}'
|
|
515
|
+
.format(response.content.decode()))
|
|
516
|
+
metadata_list = response.json()
|
|
517
|
+
|
|
518
|
+
result = []
|
|
519
|
+
for metadata in metadata_list:
|
|
520
|
+
result.append(self._build_curve(metadata))
|
|
521
|
+
return result
|