hapiclient 0.2.7__tar.gz → 0.2.9__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.
- {hapiclient-0.2.7 → hapiclient-0.2.9}/CHANGES.txt +14 -1
- {hapiclient-0.2.7/hapiclient.egg-info → hapiclient-0.2.9}/PKG-INFO +4 -2
- {hapiclient-0.2.7 → hapiclient-0.2.9}/README.md +3 -1
- {hapiclient-0.2.7 → hapiclient-0.2.9}/hapiclient/__init__.py +4 -1
- hapiclient-0.2.9/hapiclient/cache.py +255 -0
- hapiclient-0.2.9/hapiclient/capabilities.py +42 -0
- hapiclient-0.2.9/hapiclient/catalog.py +10 -0
- hapiclient-0.2.9/hapiclient/data.py +271 -0
- hapiclient-0.2.9/hapiclient/get.py +410 -0
- hapiclient-0.2.9/hapiclient/hapi.py +255 -0
- {hapiclient-0.2.7 → hapiclient-0.2.9}/hapiclient/hapitime.py +37 -50
- hapiclient-0.2.9/hapiclient/info.py +26 -0
- hapiclient-0.2.9/hapiclient/log.py +59 -0
- hapiclient-0.2.9/hapiclient/servers.py +16 -0
- {hapiclient-0.2.7 → hapiclient-0.2.9}/hapiclient/util.py +140 -158
- {hapiclient-0.2.7 → hapiclient-0.2.9/hapiclient.egg-info}/PKG-INFO +4 -2
- {hapiclient-0.2.7 → hapiclient-0.2.9}/hapiclient.egg-info/SOURCES.txt +8 -0
- {hapiclient-0.2.7 → hapiclient-0.2.9}/setup.py +1 -1
- {hapiclient-0.2.7 → hapiclient-0.2.9}/test/test_hapi_data_requests.py +32 -2
- {hapiclient-0.2.7 → hapiclient-0.2.9}/test/test_hapi_metadata_requests.py +8 -1
- hapiclient-0.2.7/hapiclient/hapi.py +0 -1126
- {hapiclient-0.2.7 → hapiclient-0.2.9}/LICENSE.txt +0 -0
- {hapiclient-0.2.7 → hapiclient-0.2.9}/MANIFEST.in +0 -0
- {hapiclient-0.2.7 → hapiclient-0.2.9}/hapiclient.egg-info/dependency_links.txt +0 -0
- {hapiclient-0.2.7 → hapiclient-0.2.9}/hapiclient.egg-info/requires.txt +0 -0
- {hapiclient-0.2.7 → hapiclient-0.2.9}/hapiclient.egg-info/top_level.txt +0 -0
- {hapiclient-0.2.7 → hapiclient-0.2.9}/setup.cfg +0 -0
- {hapiclient-0.2.7 → hapiclient-0.2.9}/test/test_chunking.py +0 -0
- {hapiclient-0.2.7 → hapiclient-0.2.9}/test/test_datetime2hapitime.py +0 -0
- {hapiclient-0.2.7 → hapiclient-0.2.9}/test/test_hapitime2datetime.py +0 -0
- {hapiclient-0.2.7 → hapiclient-0.2.9}/test/test_hapitime_reformat.py +0 -0
- {hapiclient-0.2.7 → hapiclient-0.2.9}/test/test_logging.py +0 -0
|
@@ -110,4 +110,17 @@ v0.2.7b1:
|
|
|
110
110
|
2026-04-08 -- Improve NaN handling https://github.com/hapi-server/client-python/issues/88
|
|
111
111
|
2026-05-02 -- Logging and testing https://github.com/hapi-server/client-python/issues/89
|
|
112
112
|
v0.2.7:
|
|
113
|
-
2026
|
|
113
|
+
2026-05-02 -- Support for Python 2.7 dropped. This is last release with support for 3.5 through 3.8.
|
|
114
|
+
v0.2.8b0:
|
|
115
|
+
2026-05-02 -- Documentation
|
|
116
|
+
v0.2.8b1:
|
|
117
|
+
2026-05-02 -- Versioning updates
|
|
118
|
+
2026-05-19 -- Better response error messages
|
|
119
|
+
v0.2.8b2:
|
|
120
|
+
2026-05-20 -- Refactor
|
|
121
|
+
v0.2.8:
|
|
122
|
+
2026-06-04 -- Compat. fixes for hapiplot package.
|
|
123
|
+
v0.2.8b0:
|
|
124
|
+
2026-06-08 -- Show time parsing error message
|
|
125
|
+
v0.2.9:
|
|
126
|
+
2026-06-08 -- 93-occasional-crashes-due-to-undefined-variable-dt-in-get_csv-hapiclient-version-028
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hapiclient
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.9
|
|
4
4
|
Summary: Interface to Heliophysics data server API
|
|
5
5
|
Home-page: http://pypi.python.org/pypi/hapiclient/
|
|
6
6
|
Author: Bob Weigel
|
|
@@ -28,7 +28,9 @@ Dynamic: summary
|
|
|
28
28
|
|
|
29
29
|
**HAPI Client for Python**
|
|
30
30
|
|
|
31
|
-
Basic usage examples for various HAPI servers are given in [hapi_demo.py](https://github.com/hapi-server/client-python/blob/master/hapi_demo.py)
|
|
31
|
+
Basic usage examples for various HAPI servers are given in [hapi_demo.py](https://github.com/hapi-server/client-python/blob/master/hapi_demo.py). See also
|
|
32
|
+
* Examples section of a Jupyter Notebook hosted on Google Colab: [](https://colab.research.google.com/github/hapi-server/client-python-notebooks/blob/master/hapi_demo.ipynb#examples).
|
|
33
|
+
* [PyHC Summer School tutorials](https://github.com/hapi-server/tutorial-python)
|
|
32
34
|
|
|
33
35
|
# Installation
|
|
34
36
|
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
**HAPI Client for Python**
|
|
5
5
|
|
|
6
|
-
Basic usage examples for various HAPI servers are given in [hapi_demo.py](https://github.com/hapi-server/client-python/blob/master/hapi_demo.py)
|
|
6
|
+
Basic usage examples for various HAPI servers are given in [hapi_demo.py](https://github.com/hapi-server/client-python/blob/master/hapi_demo.py). See also
|
|
7
|
+
* Examples section of a Jupyter Notebook hosted on Google Colab: [](https://colab.research.google.com/github/hapi-server/client-python-notebooks/blob/master/hapi_demo.ipynb#examples).
|
|
8
|
+
* [PyHC Summer School tutorials](https://github.com/hapi-server/tutorial-python)
|
|
7
9
|
|
|
8
10
|
# Installation
|
|
9
11
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# Allow "from hapiclient import hapi"
|
|
2
2
|
from hapiclient.hapi import hapi
|
|
3
3
|
|
|
4
|
+
# Allow "from hapiclient import request2path"
|
|
5
|
+
from hapiclient.hapi import request2path
|
|
6
|
+
|
|
4
7
|
# Allow "from hapiclient import hapitime2datetime"
|
|
5
8
|
from hapiclient.hapitime import hapitime2datetime
|
|
6
9
|
|
|
@@ -10,7 +13,7 @@ from hapiclient.hapitime import datetime2hapitime
|
|
|
10
13
|
# Allow "from hapiclient import HAPIError"
|
|
11
14
|
from hapiclient.util import HAPIError
|
|
12
15
|
|
|
13
|
-
__version__ = '0.2.
|
|
16
|
+
__version__ = '0.2.9'
|
|
14
17
|
|
|
15
18
|
import sys
|
|
16
19
|
import platform
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
def server2dirname(server):
|
|
2
|
+
"""Convert a server URL to a directory name."""
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
urld = re.sub(r"https*://", "", server)
|
|
7
|
+
urld = re.sub(r'/', '_', urld)
|
|
8
|
+
|
|
9
|
+
return urld
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def cachedir(*args):
|
|
13
|
+
"""HAPI cache directory.
|
|
14
|
+
|
|
15
|
+
cachedir() returns os.path.join(tempfile.gettempdir(), 'hapi-data')
|
|
16
|
+
|
|
17
|
+
cachedir(basedir, server) returns os.path.join(basedir, server2dirname(server))
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import tempfile
|
|
22
|
+
|
|
23
|
+
if len(args) != 0 and len(args) != 2:
|
|
24
|
+
raise ValueError('cachedir() takes either 0 or 2 arguments.')
|
|
25
|
+
|
|
26
|
+
if len(args) == 0:
|
|
27
|
+
# cachedir()
|
|
28
|
+
return os.path.join(tempfile.gettempdir(), 'hapi-data')
|
|
29
|
+
|
|
30
|
+
if len(args) == 2:
|
|
31
|
+
# cachedir(base_dir, server)
|
|
32
|
+
return os.path.join(args[0], server2dirname(args[1]))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def request2path(*args):
|
|
36
|
+
# request2path(server, dataset, parameters, start, stop)
|
|
37
|
+
# request2path(server, dataset, parameters, start, stop, cachedir)
|
|
38
|
+
import os
|
|
39
|
+
import re
|
|
40
|
+
import platform
|
|
41
|
+
|
|
42
|
+
if len(args) == 5:
|
|
43
|
+
# Use default if cachedir not given.
|
|
44
|
+
cachedirectory = cachedir()
|
|
45
|
+
else:
|
|
46
|
+
cachedirectory = args[5]
|
|
47
|
+
|
|
48
|
+
args = list(args)
|
|
49
|
+
|
|
50
|
+
# Replace forbidden characters in directory and filename
|
|
51
|
+
# Replacements assume that there will be no name collisions,
|
|
52
|
+
# e.g., one parameter named abc-< and another abc-@lt@.
|
|
53
|
+
# This also introduces an incompatibility between caches on Windows
|
|
54
|
+
# Unix.
|
|
55
|
+
if platform.system() == 'Windows':
|
|
56
|
+
# List and code from responses in
|
|
57
|
+
# https://stackoverflow.com/q/1976007
|
|
58
|
+
reps = (
|
|
59
|
+
('<', '@lt@'),
|
|
60
|
+
('>', '@gt@'),
|
|
61
|
+
(':', '@colon@'),
|
|
62
|
+
('"', '@doublequote@'),
|
|
63
|
+
('/', '@forwardslash@'),
|
|
64
|
+
('/', '@backslash@'),
|
|
65
|
+
('\\|', '@pipe@'),
|
|
66
|
+
('\\?', '@questionmark@'),
|
|
67
|
+
('\\*', '@asterisk@')
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
for element in reps:
|
|
71
|
+
args[1] = re.sub(element[0], element[1], args[1])
|
|
72
|
+
args[2] = re.sub(element[0], element[1], args[2])
|
|
73
|
+
|
|
74
|
+
else:
|
|
75
|
+
args[1] = re.sub('/','@forwardslash@',args[1])
|
|
76
|
+
args[2] = re.sub('/','@forwardslash@',args[2])
|
|
77
|
+
|
|
78
|
+
# To shorten filenames.
|
|
79
|
+
args[3] = re.sub(r'-|:|\.|Z', '', args[3])
|
|
80
|
+
args[4] = re.sub(r'-|:|\.|Z', '', args[4])
|
|
81
|
+
|
|
82
|
+
# URL subdirectory
|
|
83
|
+
urldirectory = server2dirname(args[0])
|
|
84
|
+
fname = '%s_%s_%s_%s' % (args[1], args[2], args[3], args[4])
|
|
85
|
+
|
|
86
|
+
return os.path.join(cachedirectory, urldirectory, fname)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def meta_cache_paths(SERVER, DATASET, cachedir):
|
|
90
|
+
"""Return dict with metadata cache directory and file names."""
|
|
91
|
+
|
|
92
|
+
fname_root = request2path(SERVER, DATASET, '', '', '', cachedir)
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
'json': fname_root + '.json',
|
|
96
|
+
'pkl': fname_root + '.pkl'
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def meta_cache_read(SERVER, DATASET, opts):
|
|
101
|
+
"""Read metadata from PKL cache. Returns meta dict or None."""
|
|
102
|
+
|
|
103
|
+
import os
|
|
104
|
+
import pickle
|
|
105
|
+
|
|
106
|
+
from hapiclient.log import log
|
|
107
|
+
|
|
108
|
+
if not opts["usecache"]:
|
|
109
|
+
log('Not checking metadata cache because usecache is False.')
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
fnamepkl = meta_cache_paths(SERVER, DATASET, opts['cachedir'])['pkl']
|
|
113
|
+
if os.path.isfile(fnamepkl):
|
|
114
|
+
log('Reading %s' % os.path.basename(fnamepkl))
|
|
115
|
+
with open(fnamepkl, 'rb') as f:
|
|
116
|
+
return pickle.load(f)
|
|
117
|
+
|
|
118
|
+
if opts["usecache"]:
|
|
119
|
+
log('No metadata cache file found: %s' % os.path.basename(fnamepkl))
|
|
120
|
+
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def meta_cache_write(meta, SERVER, DATASET, opts):
|
|
125
|
+
"""Write metadata to JSON and PKL cache files."""
|
|
126
|
+
|
|
127
|
+
import os
|
|
128
|
+
import json
|
|
129
|
+
import pickle
|
|
130
|
+
|
|
131
|
+
from hapiclient.log import log
|
|
132
|
+
|
|
133
|
+
if not opts["cache"]:
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
paths = meta_cache_paths(SERVER, DATASET, opts['cachedir'])
|
|
137
|
+
fnamejson, fnamepkl = paths['json'], paths['pkl']
|
|
138
|
+
|
|
139
|
+
server_dir = cachedir(opts["cachedir"], SERVER)
|
|
140
|
+
os.makedirs(server_dir, exist_ok=True)
|
|
141
|
+
|
|
142
|
+
log('Writing %s ' % os.path.basename(fnamejson))
|
|
143
|
+
with open(fnamejson, 'w') as f:
|
|
144
|
+
json.dump(meta, f, indent=4)
|
|
145
|
+
|
|
146
|
+
log('Writing %s ' % os.path.basename(fnamepkl))
|
|
147
|
+
with open(fnamepkl, 'wb') as f:
|
|
148
|
+
# protocol=2 used for Python 2.7 compatibility.
|
|
149
|
+
pickle.dump(meta, f, protocol=2)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def data_cache_paths(SERVER, DATASET, PARAMETERS, START, STOP, cachedir):
|
|
153
|
+
"""Return dict with data cache file names."""
|
|
154
|
+
|
|
155
|
+
fname_root = request2path(SERVER, DATASET, PARAMETERS, START, STOP, cachedir)
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
'csv': fname_root + '.csv',
|
|
159
|
+
'bin': fname_root + '.bin',
|
|
160
|
+
'npy': fname_root + '.npy',
|
|
161
|
+
'pkl': fname_root + '.pkl'
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def data_cache_read_metax(SERVER, DATASET, PARAMETERS, START, STOP, opts):
|
|
166
|
+
"""Read extended request metadata from PKL cache. Returns meta dict or None."""
|
|
167
|
+
|
|
168
|
+
import os
|
|
169
|
+
import pickle
|
|
170
|
+
|
|
171
|
+
from hapiclient.log import log
|
|
172
|
+
|
|
173
|
+
if not opts["usecache"]:
|
|
174
|
+
log('Not checking subsetted metadata cache because usecache is False.')
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
fnamepklx = data_cache_paths(SERVER, DATASET, PARAMETERS, START, STOP, opts['cachedir'])['pkl']
|
|
178
|
+
if os.path.isfile(fnamepklx):
|
|
179
|
+
log('Reading subsetted metadata cache %s' % os.path.basename(fnamepklx))
|
|
180
|
+
with open(fnamepklx, 'rb') as f:
|
|
181
|
+
return pickle.load(f)
|
|
182
|
+
if opts["usecache"]:
|
|
183
|
+
log('No subsetted metadata cache file found: %s' % os.path.basename(fnamepklx))
|
|
184
|
+
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def data_cache_read_npy(SERVER, DATASET, PARAMETERS, START, STOP, opts):
|
|
189
|
+
"""Read cached numpy data array. Returns None if not cached."""
|
|
190
|
+
|
|
191
|
+
import os
|
|
192
|
+
import numpy as np
|
|
193
|
+
|
|
194
|
+
from hapiclient.log import log
|
|
195
|
+
|
|
196
|
+
if not opts["usecache"]:
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
fnamenpy = data_cache_paths(SERVER, DATASET, PARAMETERS, START, STOP, opts['cachedir'])['npy']
|
|
200
|
+
|
|
201
|
+
if not os.path.isfile(fnamenpy):
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
log('Reading %s ' % os.path.basename(fnamenpy))
|
|
205
|
+
with open(fnamenpy, 'rb') as f:
|
|
206
|
+
data = np.load(f)
|
|
207
|
+
|
|
208
|
+
return data
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def data_cache_write(data_result, meta, SERVER, DATASET, PARAMETERS, START, STOP, opts):
|
|
212
|
+
"""Write data array and extended metadata to cache files.
|
|
213
|
+
|
|
214
|
+
Also updates meta with file-related x_ fields before writing.
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
import os
|
|
218
|
+
import pickle
|
|
219
|
+
import warnings
|
|
220
|
+
import numpy as np
|
|
221
|
+
|
|
222
|
+
from hapiclient.log import log
|
|
223
|
+
|
|
224
|
+
data_paths = data_cache_paths(SERVER, DATASET, PARAMETERS, START, STOP, opts['cachedir'])
|
|
225
|
+
fnamecsv, fnamebin, fnamenpy, fnamepklx = data_paths['csv'], data_paths['bin'], data_paths['npy'], data_paths['pkl']
|
|
226
|
+
|
|
227
|
+
meta_paths = meta_cache_paths(SERVER, DATASET, opts['cachedir'])
|
|
228
|
+
fnamejson, fnamepkl = meta_paths['json'], meta_paths['pkl']
|
|
229
|
+
|
|
230
|
+
meta.update({"x_metaFileParsed": fnamepkl})
|
|
231
|
+
meta.update({"x_dataFileParsed": fnamenpy})
|
|
232
|
+
meta.update({"x_metaFile": fnamejson})
|
|
233
|
+
meta.update({"x_dataFile": fnamebin if opts['format'] == 'binary' else fnamecsv})
|
|
234
|
+
|
|
235
|
+
if not opts["cache"]:
|
|
236
|
+
# Need to return after meta is updated.
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
server_dir = cachedir(opts["cachedir"], SERVER)
|
|
240
|
+
os.makedirs(server_dir, exist_ok=True)
|
|
241
|
+
|
|
242
|
+
log('Writing %s' % os.path.basename(fnamepklx))
|
|
243
|
+
with open(fnamepklx, 'wb') as f:
|
|
244
|
+
pickle.dump(meta, f, protocol=2)
|
|
245
|
+
|
|
246
|
+
log('Writing %s' % os.path.basename(fnamenpy))
|
|
247
|
+
with warnings.catch_warnings():
|
|
248
|
+
# Ignore warning that occurs when saving Unicode data.
|
|
249
|
+
kwargs = {
|
|
250
|
+
'message': r"Stored array in format 3\.0.*",
|
|
251
|
+
'category': UserWarning,
|
|
252
|
+
'module': r"numpy\.lib\.format"
|
|
253
|
+
}
|
|
254
|
+
warnings.filterwarnings("ignore", **kwargs)
|
|
255
|
+
np.save(fnamenpy, data_result)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
|
|
2
|
+
def capabilities(SERVER):
|
|
3
|
+
"""Return the capabilities of a HAPI server.
|
|
4
|
+
|
|
5
|
+
Args:
|
|
6
|
+
SERVER (str): The base URL of the HAPI server.
|
|
7
|
+
|
|
8
|
+
Returns:
|
|
9
|
+
dict: A dictionary containing the capabilities of the server.
|
|
10
|
+
"""
|
|
11
|
+
from hapiclient.util import urlopen
|
|
12
|
+
|
|
13
|
+
caps = urlopen(SERVER + '/capabilities', parse_json=True)
|
|
14
|
+
|
|
15
|
+
return caps
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_format(SERVER, format):
|
|
19
|
+
"""Return the transport format to use, accounting for server capabilities.
|
|
20
|
+
|
|
21
|
+
If the requested format is not supported by the server, falls back to 'csv'
|
|
22
|
+
with a warning.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from hapiclient.util import error, warning
|
|
26
|
+
|
|
27
|
+
cformats = ['csv', 'binary'] # client formats
|
|
28
|
+
if format not in cformats:
|
|
29
|
+
msg = 'This client does not handle transport format "%s". Available options: %s'
|
|
30
|
+
error(msg % (format, ', '.join(cformats)))
|
|
31
|
+
|
|
32
|
+
if format != 'csv':
|
|
33
|
+
caps = capabilities(SERVER)
|
|
34
|
+
sformats = caps["outputFormats"] # Server formats
|
|
35
|
+
if format not in sformats:
|
|
36
|
+
msg = 'Requested transport format "%s" not avaiable from %s. Will use "csv". Available options: %s'
|
|
37
|
+
warning(msg % (format, SERVER, ', '.join(sformats)))
|
|
38
|
+
format = 'csv'
|
|
39
|
+
if 'binary' not in sformats:
|
|
40
|
+
format = 'csv'
|
|
41
|
+
|
|
42
|
+
return format
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
def data(SERVER, DATASET, PARAMETERS, START, STOP, opts):
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import time
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from hapiclient.log import log
|
|
8
|
+
from hapiclient.util import warning, subset_meta, unicode_check, fix_parameters, missing_length
|
|
9
|
+
from hapiclient.cache import cachedir, data_cache_read_metax, data_cache_read_npy, data_cache_write
|
|
10
|
+
from hapiclient.get import get_binary, get_csv
|
|
11
|
+
from hapiclient.info import info
|
|
12
|
+
from hapiclient.capabilities import get_format
|
|
13
|
+
|
|
14
|
+
unicode_check(DATASET, PARAMETERS)
|
|
15
|
+
PARAMETERS = fix_parameters(PARAMETERS)
|
|
16
|
+
|
|
17
|
+
urld = cachedir(opts["cachedir"], SERVER)
|
|
18
|
+
if opts['usecache'] or opts['cache']:
|
|
19
|
+
log('cache subdirectory = %s' % urld)
|
|
20
|
+
|
|
21
|
+
if STOP is None:
|
|
22
|
+
log('STOP was given as None. Getting stopDate for dataset.')
|
|
23
|
+
meta = info(SERVER, DATASET, None, opts)
|
|
24
|
+
STOP = meta['stopDate']
|
|
25
|
+
log('Using STOP = {STOP}')
|
|
26
|
+
|
|
27
|
+
tic_totalTime = time.time()
|
|
28
|
+
|
|
29
|
+
meta = data_cache_read_metax(SERVER, DATASET, PARAMETERS, START, STOP, opts)
|
|
30
|
+
metaFromCache = meta is not None
|
|
31
|
+
if not metaFromCache:
|
|
32
|
+
meta = info(SERVER, DATASET, None, opts)
|
|
33
|
+
|
|
34
|
+
# Add information to metadata so we can figure out the request needed
|
|
35
|
+
# to generate it. Will also be used for labeling plots by hapiplot().
|
|
36
|
+
meta.update({"x_server": SERVER})
|
|
37
|
+
meta.update({"x_dataset": DATASET})
|
|
38
|
+
|
|
39
|
+
if opts["cache"]:
|
|
40
|
+
if not os.path.exists(urld):
|
|
41
|
+
os.makedirs(urld)
|
|
42
|
+
|
|
43
|
+
if opts['dt_chunk'] == 'infer':
|
|
44
|
+
opts['dt_chunk'] = _dt_chunk_infer(meta, opts)
|
|
45
|
+
|
|
46
|
+
if opts['n_chunks'] is not None or opts['dt_chunk'] is not None:
|
|
47
|
+
chunk_result = _get_chunks(SERVER, DATASET, PARAMETERS, START, STOP, opts, tic_totalTime)
|
|
48
|
+
if chunk_result is not None:
|
|
49
|
+
return chunk_result
|
|
50
|
+
|
|
51
|
+
if not metaFromCache:
|
|
52
|
+
meta = subset_meta(meta, PARAMETERS)
|
|
53
|
+
|
|
54
|
+
tic = time.time()
|
|
55
|
+
data_cached = data_cache_read_npy(SERVER, DATASET, PARAMETERS, START, STOP, opts)
|
|
56
|
+
if data_cached is not None:
|
|
57
|
+
meta['x_totalTime'] = time.time() - tic_totalTime
|
|
58
|
+
meta['x_readTime'] = tic - tic_totalTime
|
|
59
|
+
meta['x_downloadTime'] = 0
|
|
60
|
+
return data_cached, meta
|
|
61
|
+
|
|
62
|
+
opts['format'] = get_format(SERVER, opts['format'])
|
|
63
|
+
|
|
64
|
+
# length attribute required for all parameters when serving binary but
|
|
65
|
+
# is only required for time parameter when serving CSV. This catches
|
|
66
|
+
# case where server provides binary but is missing a length attribute
|
|
67
|
+
# in one or more string parameters that were requested. In this case,
|
|
68
|
+
# there is not enough information to parse binary.
|
|
69
|
+
if opts['format'] == 'binary' and missing_length(meta, opts):
|
|
70
|
+
warning('Requesting CSV instead of binary because a string or isotime parameter is missing a length attribute.')
|
|
71
|
+
opts['format'] = 'csv'
|
|
72
|
+
|
|
73
|
+
# Read the data. toc0 is time to download to file or into buffer;
|
|
74
|
+
# toc is time to parse.
|
|
75
|
+
if opts['format'] == 'binary':
|
|
76
|
+
data_result, toc0, toc = get_binary(meta, SERVER, DATASET, PARAMETERS, START, STOP, opts)
|
|
77
|
+
else:
|
|
78
|
+
data_result, toc0, toc = get_csv(meta, SERVER, DATASET, PARAMETERS, START, STOP, opts)
|
|
79
|
+
|
|
80
|
+
# Extra metadata associated with request will be saved in
|
|
81
|
+
# a pkl file with same base name as npy data file.
|
|
82
|
+
meta.update({"x_server": SERVER})
|
|
83
|
+
meta.update({"x_dataset": DATASET})
|
|
84
|
+
meta.update({"x_parameters": PARAMETERS})
|
|
85
|
+
meta.update({"x_time.min": START})
|
|
86
|
+
meta.update({"x_time.max": STOP})
|
|
87
|
+
meta.update({"x_requestDate": datetime.now().isoformat()[0:19]})
|
|
88
|
+
meta.update({"x_cacheDir": urld})
|
|
89
|
+
meta.update({"x_downloadTime": toc0})
|
|
90
|
+
meta.update({"x_readTime": toc})
|
|
91
|
+
|
|
92
|
+
data_cache_write(data_result, meta, SERVER, DATASET, PARAMETERS, START, STOP, opts)
|
|
93
|
+
|
|
94
|
+
meta['x_totalTime'] = time.time() - tic_totalTime
|
|
95
|
+
|
|
96
|
+
return data_result, meta
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _dt_chunk_infer(meta, opts):
|
|
100
|
+
|
|
101
|
+
import isodate
|
|
102
|
+
from datetime import datetime
|
|
103
|
+
|
|
104
|
+
cadence = meta.get('cadence', None)
|
|
105
|
+
|
|
106
|
+
# If cadence not given, use 1-day chunks.
|
|
107
|
+
if cadence is None:
|
|
108
|
+
cadence = 'PT1M'
|
|
109
|
+
else:
|
|
110
|
+
cadence = isodate.parse_duration(cadence)
|
|
111
|
+
if isinstance(cadence, isodate.Duration):
|
|
112
|
+
# When a duration does not correspond to an unambiguous
|
|
113
|
+
# time duration (e.g., P1M), parse_duration returns an
|
|
114
|
+
# isodate.duration.Duration object. Otherwise, it returns
|
|
115
|
+
# a datetime.timedelta object.
|
|
116
|
+
cadence = cadence.totimedelta(start=datetime.now())
|
|
117
|
+
|
|
118
|
+
pt1s = isodate.parse_duration('PT1S')
|
|
119
|
+
pt1h = isodate.parse_duration('PT1H')
|
|
120
|
+
p1d = isodate.parse_duration('P1D')
|
|
121
|
+
|
|
122
|
+
if cadence < pt1s:
|
|
123
|
+
return 'PT1H'
|
|
124
|
+
elif pt1s <= cadence <= pt1h:
|
|
125
|
+
return 'P1D'
|
|
126
|
+
elif cadence > pt1h:
|
|
127
|
+
return 'P1M'
|
|
128
|
+
elif cadence >= p1d:
|
|
129
|
+
return 'P1Y'
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _get_chunks(SERVER, DATASET, PARAMETERS, START, STOP, opts, tic_totalTime):
|
|
133
|
+
|
|
134
|
+
import sys
|
|
135
|
+
import time
|
|
136
|
+
import isodate
|
|
137
|
+
import numpy as np
|
|
138
|
+
|
|
139
|
+
from datetime import datetime, timedelta
|
|
140
|
+
from joblib import Parallel, delayed
|
|
141
|
+
from hapiclient.log import log
|
|
142
|
+
from hapiclient.hapitime import hapitime2datetime, hapitime_reformat
|
|
143
|
+
|
|
144
|
+
def padz(value):
|
|
145
|
+
return value if 'Z' in value else value + 'Z'
|
|
146
|
+
|
|
147
|
+
pSTART = hapitime2datetime(padz(START))[0]
|
|
148
|
+
pSTOP = hapitime2datetime(padz(STOP))[0]
|
|
149
|
+
|
|
150
|
+
if opts['dt_chunk']:
|
|
151
|
+
pDELTA = isodate.parse_duration(opts['dt_chunk'])
|
|
152
|
+
|
|
153
|
+
if opts['dt_chunk'] == 'P1Y':
|
|
154
|
+
half = isodate.parse_duration('P365D') / 2
|
|
155
|
+
elif opts['dt_chunk'] == 'P1M':
|
|
156
|
+
half = isodate.parse_duration('P30D') / 2
|
|
157
|
+
else:
|
|
158
|
+
half = pDELTA / 2
|
|
159
|
+
|
|
160
|
+
if (pSTOP - pSTART) < half:
|
|
161
|
+
opts['n_chunks'] = None
|
|
162
|
+
opts['dt_chunk'] = None
|
|
163
|
+
return data(SERVER, DATASET, PARAMETERS, START, STOP, opts)
|
|
164
|
+
|
|
165
|
+
if opts['dt_chunk'] == 'P1Y':
|
|
166
|
+
pSTART = datetime(pSTART.year, 1, 1)
|
|
167
|
+
pSTOP = datetime(pSTOP.year + 1, 1, 1)
|
|
168
|
+
opts['n_chunks'] = pSTOP.year - pSTART.year
|
|
169
|
+
elif opts['dt_chunk'] == 'P1M':
|
|
170
|
+
pSTART = datetime(pSTART.year, pSTART.month, 1)
|
|
171
|
+
pSTOP = datetime(pSTOP.year, pSTOP.month + 1, 1)
|
|
172
|
+
opts['n_chunks'] = (pSTOP.year - pSTART.year) * 12 + (pSTOP.month - pSTART.month)
|
|
173
|
+
elif opts['dt_chunk'] == 'P1D':
|
|
174
|
+
pSTART = datetime.combine(pSTART.date(), datetime.min.time())
|
|
175
|
+
pSTOP = datetime.combine(pSTOP.date(), datetime.min.time()) + timedelta(days=1)
|
|
176
|
+
opts['n_chunks'] = (pSTOP - pSTART).days
|
|
177
|
+
elif opts['dt_chunk'] == 'PT1H':
|
|
178
|
+
pSTART = datetime.combine(pSTART.date(), datetime.min.time()) + timedelta(hours=pSTART.hour)
|
|
179
|
+
pSTOP = datetime.combine(pSTOP.date(), datetime.min.time()) + timedelta(hours=pSTOP.hour + 1)
|
|
180
|
+
opts['n_chunks'] = int(((pSTOP - pSTART).total_seconds() / 60) / 60)
|
|
181
|
+
else:
|
|
182
|
+
pDIFF = pSTOP - pSTART
|
|
183
|
+
pDELTA = pDIFF / opts['n_chunks']
|
|
184
|
+
|
|
185
|
+
n_chunks = opts['n_chunks']
|
|
186
|
+
opts['n_chunks'] = None
|
|
187
|
+
opts['dt_chunk'] = None
|
|
188
|
+
|
|
189
|
+
backend = 'sequential'
|
|
190
|
+
if opts['parallel']:
|
|
191
|
+
# Note that this does not often lead to significant speed-up.
|
|
192
|
+
# Needs further testing and optimization.
|
|
193
|
+
backend = 'threading'
|
|
194
|
+
# multiprocessing not working.
|
|
195
|
+
#backend = 'multiprocessing'
|
|
196
|
+
# loky works, but not speed-up.
|
|
197
|
+
#backend = 'loky'
|
|
198
|
+
|
|
199
|
+
log('backend = {}'.format(backend))
|
|
200
|
+
|
|
201
|
+
verbose = 0
|
|
202
|
+
if opts.get('logging'):
|
|
203
|
+
verbose = 100
|
|
204
|
+
|
|
205
|
+
def nhapi(SERVER, DATASET, PARAMETERS, pSTART, pDELTA, i, **opts):
|
|
206
|
+
START = pSTART + (i * pDELTA)
|
|
207
|
+
START = str(START.date())+'T'+str(START.time())
|
|
208
|
+
|
|
209
|
+
STOP = pSTART + ((i + 1) * pDELTA)
|
|
210
|
+
STOP = str(STOP.date()) + 'T' + str(STOP.time())
|
|
211
|
+
|
|
212
|
+
data_chunk, meta = data(
|
|
213
|
+
SERVER,
|
|
214
|
+
DATASET,
|
|
215
|
+
PARAMETERS,
|
|
216
|
+
START,
|
|
217
|
+
STOP,
|
|
218
|
+
opts
|
|
219
|
+
)
|
|
220
|
+
return data_chunk, meta
|
|
221
|
+
|
|
222
|
+
resD, resM = zip(
|
|
223
|
+
*Parallel(n_jobs=opts['n_parallel'], verbose=verbose, backend=backend)(
|
|
224
|
+
delayed(nhapi)(
|
|
225
|
+
SERVER,
|
|
226
|
+
DATASET,
|
|
227
|
+
PARAMETERS,
|
|
228
|
+
pSTART,
|
|
229
|
+
pDELTA,
|
|
230
|
+
i,
|
|
231
|
+
**opts
|
|
232
|
+
) for i in range(n_chunks)
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
resD = list(resD)
|
|
237
|
+
|
|
238
|
+
tic_trimTime = time.time()
|
|
239
|
+
if sys.version_info < (3, ):
|
|
240
|
+
START = hapitime_reformat(str(resD[0]['Time'][0]), START)
|
|
241
|
+
resD[0] = resD[0][resD[0]['Time'] >= START]
|
|
242
|
+
|
|
243
|
+
STOP = hapitime_reformat(str(resD[-1]['Time'][0]), STOP)
|
|
244
|
+
resD[-1] = resD[-1][resD[-1]['Time'] < STOP]
|
|
245
|
+
else:
|
|
246
|
+
START = hapitime_reformat(resD[0]['Time'][0].decode('UTF-8'), START)
|
|
247
|
+
resD[0] = resD[0][resD[0]['Time'] >= bytes(START, 'UTF-8')]
|
|
248
|
+
|
|
249
|
+
STOP = hapitime_reformat(resD[-1]['Time'][0].decode('UTF-8'), STOP)
|
|
250
|
+
resD[-1] = resD[-1][resD[-1]['Time'] < bytes(STOP, 'UTF-8')]
|
|
251
|
+
trimTime = time.time() - tic_trimTime
|
|
252
|
+
|
|
253
|
+
tic_catTime = time.time()
|
|
254
|
+
data_concat = np.concatenate(resD)
|
|
255
|
+
catTime = time.time() - tic_catTime
|
|
256
|
+
|
|
257
|
+
meta = resM[0].copy()
|
|
258
|
+
meta['x_time.max'] = resM[-1]['x_time.max']
|
|
259
|
+
meta['x_dataFile'] = None
|
|
260
|
+
meta['x_dataFiles'] = [resM[i]['x_dataFile'] for i in range(len(resM))]
|
|
261
|
+
meta['x_downloadTime'] = sum([resM[i]['x_downloadTime'] for i in range(len(resM))])
|
|
262
|
+
meta['x_downloadTimes'] = [resM[i]['x_downloadTime'] for i in range(len(resM))]
|
|
263
|
+
meta['x_readTime'] = sum([resM[i]['x_readTime'] for i in range(len(resM))])
|
|
264
|
+
meta['x_readTimes'] = [resM[i]['x_readTime'] for i in range(len(resM))]
|
|
265
|
+
meta['x_trimTime'] = trimTime
|
|
266
|
+
meta['x_catTime'] = catTime
|
|
267
|
+
meta['x_totalTime'] = time.time() - tic_totalTime
|
|
268
|
+
meta['x_dataFileParsed'] = None
|
|
269
|
+
meta['x_dataFilesParsed'] = [resM[i]['x_dataFileParsed'] for i in range(len(resM))]
|
|
270
|
+
|
|
271
|
+
return data_concat, meta
|