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.
Files changed (32) hide show
  1. {hapiclient-0.2.7 → hapiclient-0.2.9}/CHANGES.txt +14 -1
  2. {hapiclient-0.2.7/hapiclient.egg-info → hapiclient-0.2.9}/PKG-INFO +4 -2
  3. {hapiclient-0.2.7 → hapiclient-0.2.9}/README.md +3 -1
  4. {hapiclient-0.2.7 → hapiclient-0.2.9}/hapiclient/__init__.py +4 -1
  5. hapiclient-0.2.9/hapiclient/cache.py +255 -0
  6. hapiclient-0.2.9/hapiclient/capabilities.py +42 -0
  7. hapiclient-0.2.9/hapiclient/catalog.py +10 -0
  8. hapiclient-0.2.9/hapiclient/data.py +271 -0
  9. hapiclient-0.2.9/hapiclient/get.py +410 -0
  10. hapiclient-0.2.9/hapiclient/hapi.py +255 -0
  11. {hapiclient-0.2.7 → hapiclient-0.2.9}/hapiclient/hapitime.py +37 -50
  12. hapiclient-0.2.9/hapiclient/info.py +26 -0
  13. hapiclient-0.2.9/hapiclient/log.py +59 -0
  14. hapiclient-0.2.9/hapiclient/servers.py +16 -0
  15. {hapiclient-0.2.7 → hapiclient-0.2.9}/hapiclient/util.py +140 -158
  16. {hapiclient-0.2.7 → hapiclient-0.2.9/hapiclient.egg-info}/PKG-INFO +4 -2
  17. {hapiclient-0.2.7 → hapiclient-0.2.9}/hapiclient.egg-info/SOURCES.txt +8 -0
  18. {hapiclient-0.2.7 → hapiclient-0.2.9}/setup.py +1 -1
  19. {hapiclient-0.2.7 → hapiclient-0.2.9}/test/test_hapi_data_requests.py +32 -2
  20. {hapiclient-0.2.7 → hapiclient-0.2.9}/test/test_hapi_metadata_requests.py +8 -1
  21. hapiclient-0.2.7/hapiclient/hapi.py +0 -1126
  22. {hapiclient-0.2.7 → hapiclient-0.2.9}/LICENSE.txt +0 -0
  23. {hapiclient-0.2.7 → hapiclient-0.2.9}/MANIFEST.in +0 -0
  24. {hapiclient-0.2.7 → hapiclient-0.2.9}/hapiclient.egg-info/dependency_links.txt +0 -0
  25. {hapiclient-0.2.7 → hapiclient-0.2.9}/hapiclient.egg-info/requires.txt +0 -0
  26. {hapiclient-0.2.7 → hapiclient-0.2.9}/hapiclient.egg-info/top_level.txt +0 -0
  27. {hapiclient-0.2.7 → hapiclient-0.2.9}/setup.cfg +0 -0
  28. {hapiclient-0.2.7 → hapiclient-0.2.9}/test/test_chunking.py +0 -0
  29. {hapiclient-0.2.7 → hapiclient-0.2.9}/test/test_datetime2hapitime.py +0 -0
  30. {hapiclient-0.2.7 → hapiclient-0.2.9}/test/test_hapitime2datetime.py +0 -0
  31. {hapiclient-0.2.7 → hapiclient-0.2.9}/test/test_hapitime_reformat.py +0 -0
  32. {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--05-02-- Support for Python 2.7 dropped. This is last release with support for 3.5 through 3.8.
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.7
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) and the Examples section of a Jupyter Notebook hosted on Google Colab: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/hapi-server/client-python-notebooks/blob/master/hapi_demo.ipynb#examples).
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: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](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) and the Examples section of a Jupyter Notebook hosted on Google Colab: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/hapi-server/client-python-notebooks/blob/master/hapi_demo.ipynb#examples).
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: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](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.7'
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,10 @@
1
+ from hapiclient.log import log
2
+ from hapiclient.util import urlopen
3
+
4
+
5
+ def catalog(SERVER):
6
+ # TODO: Cache
7
+ url = SERVER + '/catalog'
8
+ meta = urlopen(url, parse_json=True)
9
+
10
+ return meta
@@ -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