hapiclient 0.2.5__tar.gz → 0.2.7__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 (28) hide show
  1. {hapiclient-0.2.5 → hapiclient-0.2.7}/CHANGES.txt +18 -1
  2. {hapiclient-0.2.5 → hapiclient-0.2.7}/PKG-INFO +59 -35
  3. hapiclient-0.2.5/hapiclient.egg-info/PKG-INFO → hapiclient-0.2.7/README.md +43 -44
  4. {hapiclient-0.2.5 → hapiclient-0.2.7}/hapiclient/__init__.py +10 -1
  5. {hapiclient-0.2.5 → hapiclient-0.2.7}/hapiclient/hapi.py +139 -43
  6. {hapiclient-0.2.5 → hapiclient-0.2.7}/hapiclient/hapitime.py +53 -18
  7. {hapiclient-0.2.5 → hapiclient-0.2.7}/hapiclient/util.py +42 -21
  8. hapiclient-0.2.5/README.md → hapiclient-0.2.7/hapiclient.egg-info/PKG-INFO +68 -30
  9. {hapiclient-0.2.5 → hapiclient-0.2.7}/hapiclient.egg-info/SOURCES.txt +7 -6
  10. {hapiclient-0.2.5 → hapiclient-0.2.7}/setup.py +6 -3
  11. {hapiclient-0.2.5/hapiclient → hapiclient-0.2.7}/test/test_chunking.py +85 -57
  12. hapiclient-0.2.7/test/test_datetime2hapitime.py +32 -0
  13. hapiclient-0.2.7/test/test_hapi_data_requests.py +224 -0
  14. hapiclient-0.2.7/test/test_hapi_metadata_requests.py +145 -0
  15. hapiclient-0.2.7/test/test_hapitime2datetime.py +102 -0
  16. hapiclient-0.2.7/test/test_hapitime_reformat.py +83 -0
  17. hapiclient-0.2.7/test/test_logging.py +137 -0
  18. hapiclient-0.2.5/hapiclient/test/__init__.py +0 -0
  19. hapiclient-0.2.5/hapiclient/test/compare.py +0 -240
  20. hapiclient-0.2.5/hapiclient/test/test_hapi.py +0 -310
  21. hapiclient-0.2.5/hapiclient/test/test_hapitime2datetime.py +0 -124
  22. hapiclient-0.2.5/hapiclient/test/test_hapitime_reformat.py +0 -79
  23. {hapiclient-0.2.5 → hapiclient-0.2.7}/LICENSE.txt +0 -0
  24. {hapiclient-0.2.5 → hapiclient-0.2.7}/MANIFEST.in +0 -0
  25. {hapiclient-0.2.5 → hapiclient-0.2.7}/hapiclient.egg-info/dependency_links.txt +0 -0
  26. {hapiclient-0.2.5 → hapiclient-0.2.7}/hapiclient.egg-info/requires.txt +1 -1
  27. {hapiclient-0.2.5 → hapiclient-0.2.7}/hapiclient.egg-info/top_level.txt +0 -0
  28. {hapiclient-0.2.5 → hapiclient-0.2.7}/setup.cfg +0 -0
@@ -10,7 +10,7 @@ v0.0.6:
10
10
  2018-10-04 -- Improve dateticks.py
11
11
  2018-10-04 -- Add hapi_demo.ipynb
12
12
  2018-10-04 -- Other documentation improvements
13
- v0.0.7:
13
+ v0.0.7:
14
14
  2018-10-06 -- Improve documentation
15
15
  2018-10-06 -- Add reader tests
16
16
  2018-10-23 -- Improve dateticks and tests
@@ -94,3 +94,20 @@ v0.2.4:
94
94
  v0.2.5:
95
95
  2022-06-30 -- 10df388 Update docstring for hapi()
96
96
  2022-07-12 -- 460c30b Final Unicode/Windows fixes
97
+ v0.2.6b:
98
+ 2022-08-08 -- 508fb20-3f1168c Add datetime2hapitime
99
+ 2022-08-08 -- 8bfef2f Allow start and stop to be None; https://github.com/hapi-server/client-python/issues/10
100
+ 2022-09-21 -- bc70f3c CSV read failure; https://github.com/hapi-server/client-python/issues/62
101
+ 2022-09-22 -- 373fdca Catch other CSV read failures
102
+ v0.2.6b1:
103
+ 2023-02-04 -- 2ef7cff Unicode fix; Log string fix
104
+ 2024-05-11 -- fba4ad3 Testing Updates
105
+ v0.2.6:
106
+ 2023-05-24 -- Use token for PyPi in Makefile
107
+ v0.2.7b1:
108
+ 2025-03-01 -- Unrecognized ISO 8601 time format: '00:00:00.Z' https://github.com/hapi-server/client-python/issues/76
109
+ 2025-03-01 -- 'infer_datetime_format' is deprecated ... https://github.com/hapi-server/client-python/issues/78
110
+ 2026-04-08 -- Improve NaN handling https://github.com/hapi-server/client-python/issues/88
111
+ 2026-05-02 -- Logging and testing https://github.com/hapi-server/client-python/issues/89
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.
@@ -1,14 +1,27 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: hapiclient
3
- Version: 0.2.5
3
+ Version: 0.2.7
4
4
  Summary: Interface to Heliophysics data server API
5
5
  Home-page: http://pypi.python.org/pypi/hapiclient/
6
6
  Author: Bob Weigel
7
7
  Author-email: rweigel@gmu.edu
8
8
  License: LICENSE.txt
9
- Platform: UNKNOWN
10
9
  Description-Content-Type: text/markdown
11
10
  License-File: LICENSE.txt
11
+ Requires-Dist: urllib3
12
+ Requires-Dist: joblib
13
+ Requires-Dist: isodate>=0.6.0
14
+ Requires-Dist: pandas>=0.23
15
+ Requires-Dist: numpy>=1.14.3
16
+ Dynamic: author
17
+ Dynamic: author-email
18
+ Dynamic: description
19
+ Dynamic: description-content-type
20
+ Dynamic: home-page
21
+ Dynamic: license
22
+ Dynamic: license-file
23
+ Dynamic: requires-dist
24
+ Dynamic: summary
12
25
 
13
26
  [![DOI](https://zenodo.org/badge/93170857.svg)](https://zenodo.org/badge/latestdoi/93170857)
14
27
  [![Build Status](https://app.travis-ci.com/hapi-server/client-python.svg?branch=master)](https://app.travis-ci.com/hapi-server/client-python)
@@ -30,9 +43,9 @@ The optional [hapiplot package](https://github.com/hapi-server/plot-python) prov
30
43
  To install `hapiplot`, use
31
44
 
32
45
  ```bash
33
- pip install hapiplot --upgrade
46
+ python -m pip install hapiplot --upgrade
34
47
  # or
35
- pip install 'git+https://github.com/hapi-server/plot-python' --upgrade
48
+ python -m pip install 'git+https://github.com/hapi-server/plot-python' --upgrade
36
49
  ```
37
50
 
38
51
  See the [Appendix](#appendix) for a fail-safe installation method.
@@ -51,6 +64,7 @@ start = '2003-09-01T00:00:00'
51
64
  stop = '2003-12-01T00:00:00'
52
65
  parameters = 'DST1800'
53
66
  opts = {'logging': True}
67
+ # See misc/hapi_logging_demo.py for example of using conventional Python logging.
54
68
 
55
69
  # Get data
56
70
  data, meta = hapi(server, dataset, parameters, start, stop, **opts)
@@ -93,11 +107,45 @@ returns the [Numpy N-D array](https://docs.scipy.org/doc/numpy-1.15.1/user/quick
93
107
 
94
108
  The HAPI client data model is intentionally basic. There is an ongoing discussion of a data model for Heliophysics data among the [PyHC community](https://heliopython.org/). When this data model is complete, a function that converts `data` and `meta` to that data model will be included in the `hapiclient` package.
95
109
 
96
- # Development
110
+ # Development and Testing
111
+
112
+ ## Testing Python versions available with tox-env
113
+
114
+ As of 2026-04-14, Python 3.8+ is available with tox-env.
97
115
 
98
116
  ```bash
99
117
  git clone https://github.com/hapi-server/client-python
100
- cd client-python; pip install -e .
118
+ cd client-python; python -m pip install -e .
119
+
120
+ # Run a specific test on current Python version (default is log_level=INFO
121
+ # for hapiclient logger and test logger when executed this way).
122
+ python test/test_hapi_data_requests.py
123
+
124
+ # Run a specific test on a specific Python version with all logging
125
+ tox -e py311 -- test/test_hapi_data_requests.py
126
+ # Set log level for test logger
127
+ tox -e py311 -- test/test_hapi_data_requests.py --log-level=INFO
128
+
129
+ # Run a specific test on all Python versions set by tox.ini `envlist`
130
+ tox -- test/test_hapi_data_requests.py
131
+
132
+ # Run all tests in ./test
133
+ tox
134
+
135
+ # Run long-running tests
136
+ tox -e long-test
137
+
138
+ # Run long-running tests on a specific Python version
139
+ tox -e long-test --override testenv:long-test.basepython=python3.11
140
+ ```
141
+
142
+ ## Testing Python versions available with Anaconda
143
+
144
+ As of 2026-04-14, Python 3.5+ is available with Anaconda.
145
+
146
+ ```bash
147
+ git clone https://github.com/hapi-server/client-python
148
+ cd client-python; python -m pip install -e .
101
149
  ```
102
150
 
103
151
  or, create an isolated Anaconda installation (downloads and installs latest Miniconda3) using
@@ -108,7 +156,7 @@ make install PYTHON=python3.6
108
156
  ```
109
157
 
110
158
  The command `pip install -e .` creates symlinks so that the local package is
111
- used instead of an installed package. You may need to execute `pip uninstall hapiclient` to ensure the local package is used. To check the version installed, use `pip list | grep hapiclient`.
159
+ used instead of an installed package. You may need to execute `python -m pip uninstall hapiclient` to ensure the local package is used. To check the version installed, use `python -m pip list | grep hapiclient`.
112
160
 
113
161
  To run tests before a commit, execute
114
162
 
@@ -118,9 +166,8 @@ make repository-test
118
166
 
119
167
  To run an individual unit test in a Python session, use, e.g.,
120
168
 
121
- ```python
122
- from hapiclient.test.test_hapi import test_reader_short
123
- test_reader_short()
169
+ ```bash
170
+ cd test; python test_hapitime_reformat.py
124
171
  ```
125
172
 
126
173
  # Contact
@@ -128,27 +175,4 @@ test_reader_short()
128
175
  Submit bug reports and feature requests on the [repository issue
129
176
  tracker](https://github.com/hapi-server/client-python/issues>).
130
177
 
131
- # Appendix
132
-
133
- Fail-safe installation
134
-
135
- Python command line:
136
-
137
- ```python
138
- import os
139
- print(os.popen("pip install hapiclient").read())
140
- ```
141
-
142
- The above executes and displays the output of the operating system
143
- command `pip install hapiclient` using the shell environment
144
- associated with that installation of Python.
145
-
146
- This method addresses a problem that is sometimes encountered when
147
- attempting to use `pip` packages in Anaconda. To use a `pip` package
148
- in Anaconda, one must use the version of `pip` installed with Anaconda
149
- (it is usually under a subdirectory with the name `anaconda/`) as
150
- opposed to the one installed with the operating system. To see the
151
- location of ``pip`` used in a given Python session, enter
152
- `print(os.popen("which pip").read())`.
153
-
154
-
178
+ Before submitting a pull request, please post an issue with the proposed changes for discussion prior to developing the pull request.
@@ -1,15 +1,3 @@
1
- Metadata-Version: 2.1
2
- Name: hapiclient
3
- Version: 0.2.5
4
- Summary: Interface to Heliophysics data server API
5
- Home-page: http://pypi.python.org/pypi/hapiclient/
6
- Author: Bob Weigel
7
- Author-email: rweigel@gmu.edu
8
- License: LICENSE.txt
9
- Platform: UNKNOWN
10
- Description-Content-Type: text/markdown
11
- License-File: LICENSE.txt
12
-
13
1
  [![DOI](https://zenodo.org/badge/93170857.svg)](https://zenodo.org/badge/latestdoi/93170857)
14
2
  [![Build Status](https://app.travis-ci.com/hapi-server/client-python.svg?branch=master)](https://app.travis-ci.com/hapi-server/client-python)
15
3
 
@@ -30,9 +18,9 @@ The optional [hapiplot package](https://github.com/hapi-server/plot-python) prov
30
18
  To install `hapiplot`, use
31
19
 
32
20
  ```bash
33
- pip install hapiplot --upgrade
21
+ python -m pip install hapiplot --upgrade
34
22
  # or
35
- pip install 'git+https://github.com/hapi-server/plot-python' --upgrade
23
+ python -m pip install 'git+https://github.com/hapi-server/plot-python' --upgrade
36
24
  ```
37
25
 
38
26
  See the [Appendix](#appendix) for a fail-safe installation method.
@@ -51,6 +39,7 @@ start = '2003-09-01T00:00:00'
51
39
  stop = '2003-12-01T00:00:00'
52
40
  parameters = 'DST1800'
53
41
  opts = {'logging': True}
42
+ # See misc/hapi_logging_demo.py for example of using conventional Python logging.
54
43
 
55
44
  # Get data
56
45
  data, meta = hapi(server, dataset, parameters, start, stop, **opts)
@@ -93,11 +82,45 @@ returns the [Numpy N-D array](https://docs.scipy.org/doc/numpy-1.15.1/user/quick
93
82
 
94
83
  The HAPI client data model is intentionally basic. There is an ongoing discussion of a data model for Heliophysics data among the [PyHC community](https://heliopython.org/). When this data model is complete, a function that converts `data` and `meta` to that data model will be included in the `hapiclient` package.
95
84
 
96
- # Development
85
+ # Development and Testing
86
+
87
+ ## Testing Python versions available with tox-env
88
+
89
+ As of 2026-04-14, Python 3.8+ is available with tox-env.
90
+
91
+ ```bash
92
+ git clone https://github.com/hapi-server/client-python
93
+ cd client-python; python -m pip install -e .
94
+
95
+ # Run a specific test on current Python version (default is log_level=INFO
96
+ # for hapiclient logger and test logger when executed this way).
97
+ python test/test_hapi_data_requests.py
98
+
99
+ # Run a specific test on a specific Python version with all logging
100
+ tox -e py311 -- test/test_hapi_data_requests.py
101
+ # Set log level for test logger
102
+ tox -e py311 -- test/test_hapi_data_requests.py --log-level=INFO
103
+
104
+ # Run a specific test on all Python versions set by tox.ini `envlist`
105
+ tox -- test/test_hapi_data_requests.py
106
+
107
+ # Run all tests in ./test
108
+ tox
109
+
110
+ # Run long-running tests
111
+ tox -e long-test
112
+
113
+ # Run long-running tests on a specific Python version
114
+ tox -e long-test --override testenv:long-test.basepython=python3.11
115
+ ```
116
+
117
+ ## Testing Python versions available with Anaconda
118
+
119
+ As of 2026-04-14, Python 3.5+ is available with Anaconda.
97
120
 
98
121
  ```bash
99
122
  git clone https://github.com/hapi-server/client-python
100
- cd client-python; pip install -e .
123
+ cd client-python; python -m pip install -e .
101
124
  ```
102
125
 
103
126
  or, create an isolated Anaconda installation (downloads and installs latest Miniconda3) using
@@ -108,7 +131,7 @@ make install PYTHON=python3.6
108
131
  ```
109
132
 
110
133
  The command `pip install -e .` creates symlinks so that the local package is
111
- used instead of an installed package. You may need to execute `pip uninstall hapiclient` to ensure the local package is used. To check the version installed, use `pip list | grep hapiclient`.
134
+ used instead of an installed package. You may need to execute `python -m pip uninstall hapiclient` to ensure the local package is used. To check the version installed, use `python -m pip list | grep hapiclient`.
112
135
 
113
136
  To run tests before a commit, execute
114
137
 
@@ -118,9 +141,8 @@ make repository-test
118
141
 
119
142
  To run an individual unit test in a Python session, use, e.g.,
120
143
 
121
- ```python
122
- from hapiclient.test.test_hapi import test_reader_short
123
- test_reader_short()
144
+ ```bash
145
+ cd test; python test_hapitime_reformat.py
124
146
  ```
125
147
 
126
148
  # Contact
@@ -128,27 +150,4 @@ test_reader_short()
128
150
  Submit bug reports and feature requests on the [repository issue
129
151
  tracker](https://github.com/hapi-server/client-python/issues>).
130
152
 
131
- # Appendix
132
-
133
- Fail-safe installation
134
-
135
- Python command line:
136
-
137
- ```python
138
- import os
139
- print(os.popen("pip install hapiclient").read())
140
- ```
141
-
142
- The above executes and displays the output of the operating system
143
- command `pip install hapiclient` using the shell environment
144
- associated with that installation of Python.
145
-
146
- This method addresses a problem that is sometimes encountered when
147
- attempting to use `pip` packages in Anaconda. To use a `pip` package
148
- in Anaconda, one must use the version of `pip` installed with Anaconda
149
- (it is usually under a subdirectory with the name `anaconda/`) as
150
- opposed to the one installed with the operating system. To see the
151
- location of ``pip`` used in a given Python session, enter
152
- `print(os.popen("which pip").read())`.
153
-
154
-
153
+ Before submitting a pull request, please post an issue with the proposed changes for discussion prior to developing the pull request.
@@ -4,12 +4,21 @@ from hapiclient.hapi import hapi
4
4
  # Allow "from hapiclient import hapitime2datetime"
5
5
  from hapiclient.hapitime import hapitime2datetime
6
6
 
7
+ # Allow "from hapiclient import datetime2hapitime"
8
+ from hapiclient.hapitime import datetime2hapitime
9
+
7
10
  # Allow "from hapiclient import HAPIError"
8
11
  from hapiclient.util import HAPIError
9
12
 
10
- __version__ = '0.2.5'
13
+ __version__ = '0.2.7'
11
14
 
12
15
  import sys
16
+ import platform
17
+
18
+ if sys.version_info < (3, 10) and platform.system() == "Darwin":
19
+ import warnings
20
+ warnings.filterwarnings("ignore", message=".*urllib3.*OpenSSL.*")
21
+
13
22
  if sys.version_info[0] < 3:
14
23
  # Python 2.7
15
24
  reload(sys)
@@ -4,6 +4,7 @@ import sys
4
4
  import json
5
5
  import time
6
6
  import pickle
7
+ import logging as _logging
7
8
  import warnings
8
9
  from datetime import datetime, timedelta
9
10
 
@@ -13,7 +14,7 @@ import numpy as np
13
14
  from joblib import Parallel, delayed
14
15
 
15
16
  from hapiclient.hapitime import hapitime2datetime, hapitime_reformat
16
- from hapiclient.util import setopts, log, warning, error
17
+ from hapiclient.util import setopts, log, warning, error, configure_logging
17
18
  from hapiclient.util import urlopen, urlretrieve, jsonparse, unicode_error_message
18
19
 
19
20
 
@@ -176,7 +177,7 @@ def hapiopts():
176
177
  def hapi(*args, **kwargs):
177
178
  """Request data from a HAPI server.
178
179
 
179
- Version: 0.2.5
180
+ Version: 0.2.7
180
181
 
181
182
 
182
183
  Examples
@@ -194,10 +195,10 @@ def hapi(*args, **kwargs):
194
195
  A comma-separated list of parameters in `dataset`
195
196
  start: str
196
197
  The start time of the requested data
197
- stop: str
198
+ stop: str or None
198
199
  The end time of the requested data; end times are exclusive - the
199
200
  last data record returned by a HAPI server should have a timestamp
200
- before `start`.
201
+ before `start`. If `None`, `stopDate` is used.
201
202
  options: dict
202
203
  `logging` (``False``) - Log to console
203
204
 
@@ -250,7 +251,7 @@ def hapi(*args, **kwargs):
250
251
  Cadence = PT1M and request for
251
252
 
252
253
  start/stop=1999-11-12T00:10:00/stop=1999-11-12T12:09:00
253
-
254
+
254
255
  Chunk size is P1D and requested time range < 1/2 of this
255
256
  => Default behavior
256
257
 
@@ -337,14 +338,18 @@ def hapi(*args, **kwargs):
337
338
  START = START + 'Z'
338
339
  if nin > 4:
339
340
  STOP = args[4]
340
- if STOP[-1] != 'Z':
341
+ if STOP is not None and STOP[-1] != 'Z':
341
342
  # TODO: Consider warning.
342
343
  STOP = STOP + 'Z'
343
344
 
344
345
  # Override defaults
345
346
  opts = setopts(hapiopts(), kwargs)
346
347
 
347
- assert (opts['logging'] in [True, False]), "logging keyword must be True of False"
348
+ assert (isinstance(opts['logging'], bool) or hasattr(opts['logging'], 'write')), \
349
+ "logging keyword must be True, False, or a file-like object"
350
+
351
+ configure_logging(opts)
352
+
348
353
  assert (opts['cache'] in [True, False]), "cache keyword must be True of False"
349
354
  assert (opts['usecache'] in [True, False]), "usecache keyword must be True of False"
350
355
  assert (opts['format'] in ['binary', 'csv']), "format keyword must be 'csv' or 'binary'"
@@ -408,7 +413,7 @@ def hapi(*args, **kwargs):
408
413
  # urld = url subdirectory of cachedir to store files from SERVER
409
414
  urld = cachedir(opts["cachedir"], SERVER)
410
415
 
411
- if opts["cachedir"]: log('file directory = %s' % urld, opts)
416
+ if opts["cachedir"]: log('cache subdirectory = %s' % urld, opts)
412
417
 
413
418
  urljson = SERVER + '/info?id=' + DATASET
414
419
 
@@ -422,6 +427,12 @@ def hapi(*args, **kwargs):
422
427
 
423
428
  if nin == 5: # Data requested
424
429
 
430
+ if STOP is None:
431
+ log('STOP was given as None. Getting stopDate for dataset.', opts)
432
+ meta = hapi(SERVER, DATASET)
433
+ STOP = meta['stopDate']
434
+ log('Using STOP = {STOP}', opts)
435
+
425
436
  tic_totalTime = time.time()
426
437
 
427
438
  # URL to get CSV (will be used if binary response is not available)
@@ -552,10 +563,13 @@ def hapi(*args, **kwargs):
552
563
 
553
564
  backend = 'sequential'
554
565
  if opts['parallel']:
566
+ # Note that this does not often lead to significant speed-up.
567
+ # Needs further testing and optimization.
555
568
  backend = 'threading'
556
- # multiprocessing was not tested. It may work, but will
557
- # need a speed comparison with threading.
558
- # backend = 'multiprocessing'
569
+ # multiprocessing not working.
570
+ #backend = 'multiprocessing'
571
+ # loky works, but not speed-up.
572
+ #backend = 'loky'
559
573
 
560
574
  log('backend = {}'.format(backend), opts)
561
575
 
@@ -657,14 +671,18 @@ def hapi(*args, **kwargs):
657
671
  if opts["usecache"] and os.path.isfile(fnamenpy):
658
672
  # Read cached data file.
659
673
  log('Reading %s ' % fnamenpy.replace(urld + '/', ''), opts)
674
+ tic = time.time()
660
675
  f = open(fnamenpy, 'rb')
661
676
  data = np.load(f)
662
677
  f.close()
678
+ toc = time.time() - tic
663
679
  # There is a possibility that the fnamenpy file existed but
664
680
  # fnamepklx was not found (b/c removed). In this case, the meta
665
681
  # returned will not have all of the "x_" information inserted below.
666
682
  # Code that uses this information needs to account for this.
667
683
  meta['x_totalTime'] = time.time() - tic_totalTime
684
+ meta['x_readTime'] = tic - tic_totalTime
685
+ meta['x_downloadTime'] = 0
668
686
  return data, meta
669
687
 
670
688
  cformats = ['csv', 'binary'] # client formats
@@ -728,7 +746,7 @@ def hapi(*args, **kwargs):
728
746
  try:
729
747
  data = np.fromfile(fnamebin, dtype=dt)
730
748
  except:
731
- error('Could not read response from {}'.format(urlbin))
749
+ error('Malformed response? Could not read: {}'.format(urlbin))
732
750
  else:
733
751
  from io import BytesIO
734
752
  log('Writing %s to buffer' % urlbin.replace(urld + '/', ''), opts)
@@ -740,7 +758,8 @@ def hapi(*args, **kwargs):
740
758
  try:
741
759
  data = np.frombuffer(buff.read(), dtype=dt)
742
760
  except:
743
- error('Could not read response from {}'.format(urlbin))
761
+ error('Malformed response? Could not read: {}'.format(urlbin))
762
+
744
763
 
745
764
  # Handle Unicode
746
765
  time_name = meta['parameters'][0]['name']
@@ -753,7 +772,7 @@ def hapi(*args, **kwargs):
753
772
  # with Unicode, it automatically converts Unicode chars to
754
773
  # slash encoded ASCII.
755
774
  name = str(name)
756
- if isinstance(dt[i][1], str) and 'U' in dto[i][1]:
775
+ if data[name].size > 0 and isinstance(dt[i][1], str) and 'U' in dto[i][1]:
757
776
  # Decode data.
758
777
  datanew[name] = np.char.decode(data[name])
759
778
  else:
@@ -763,6 +782,8 @@ def hapi(*args, **kwargs):
763
782
  else:
764
783
  # HAPI CSV
765
784
 
785
+ file_empty = False
786
+
766
787
  if opts["cache"]:
767
788
  log('Writing %s to %s' % (urlcsv, fnamecsv.replace(urld + '/', '')), opts)
768
789
  tic0 = time.time()
@@ -770,38 +791,78 @@ def hapi(*args, **kwargs):
770
791
  toc0 = time.time() - tic0
771
792
  log('Reading and parsing %s' % fnamecsv.replace(urld + '/', ''), opts)
772
793
  tic = time.time()
794
+ if os.path.getsize(fnamecsv) == 0:
795
+ file_empty = True
796
+ data = np.array([], dtype=dt)
773
797
  else:
774
798
  from io import StringIO
775
- #log('Writing %s to buffer' % urlcsv.replace(urld + '/', ''), opts)
799
+ log('Writing %s to buffer' % urlcsv.replace(urld + '/', ''), opts)
776
800
  tic0 = time.time()
777
801
  fnamecsv = StringIO(urlopen(urlcsv).read().decode())
802
+ fnamecsv.seek(0, os.SEEK_END)
803
+ if fnamecsv.tell() == 0:
804
+ file_empty = True
805
+ data = np.array([], dtype=dt)
806
+ else:
807
+ fnamecsv.seek(0)
778
808
  toc0 = time.time() - tic0
779
809
  log('Parsing StringIO buffer.', opts)
780
810
  tic = time.time()
781
811
 
782
- if not missing_length:
783
- # All string and isotime parameters have a length in metadata.
784
- if opts['method'] == 'numpy':
785
- data = np.genfromtxt(fnamecsv, dtype=dt, delimiter=',',
786
- replace_space=' ',
787
- deletechars='', encoding='utf-8')
788
- if opts['method'] == '' or opts['method'] == 'pandas':
789
- # Read file into Pandas DataFrame
790
- df = pandas.read_csv(fnamecsv, sep=',', header=None,
791
- encoding='utf-8')
792
- # Allocate output N-D array (It is not possible to pass dtype=dt
793
- # as computed to pandas.read_csv; pandas dtype is different
794
- # from numpy's dtype.)
795
- data = np.ndarray(shape=(len(df)), dtype=dt)
796
- # Insert data from dataframe 'df' columns into N-D array 'data'
797
- for i in range(0, len(pnames)):
798
- shape = np.append(len(data), psizes[i])
799
- # In numpy 1.8.2 and Python 2.7, this throws an error
800
- # for no apparent reason. Works as expected in numpy 1.10.4
801
- data[pnames[i]] = np.squeeze(
802
- np.reshape(df.values[:, np.arange(cols[i][0], cols[i][1] + 1)], shape))
803
- else:
804
- data = parse_missing_length(fnamecsv, dt, cols, psizes, pnames, ptypes, opts)
812
+ if file_empty == False:
813
+ if not missing_length:
814
+ # All string and isotime parameters have a length in metadata.
815
+ if opts['method'] == 'numpy':
816
+ try:
817
+ data = np.genfromtxt(fnamecsv,
818
+ dtype=dt,
819
+ delimiter=',',
820
+ replace_space=' ',
821
+ deletechars='',
822
+ encoding='utf-8')
823
+ except:
824
+ error('Malformed response? Could not read response: {}'.format(urlcsv))
825
+ if opts['method'] == '' or opts['method'] == 'pandas':
826
+ # Read file into Pandas DataFrame
827
+ csv_kwargs = {
828
+ 'sep': ',',
829
+ 'header': None,
830
+ 'encoding': 'utf-8',
831
+ 'skipinitialspace': True,
832
+ 'keep_default_na': False,
833
+ 'na_values': ['NaN', 'nan', 'Nan', 'naN', ' "NaN"', ' "nan"', ' "Nan"', ' "naN"', '"NaN"', '"nan"', '"Nan"', '"naN"']
834
+ }
835
+ """
836
+ Note that this does not handle trailing whitespace after
837
+ any of the na_values. (There is no skiptrailingspace option).
838
+ Stripping trailing whitespace would require adding something
839
+ like
840
+ def strip_field(x):
841
+ # Strip whitespace and normalize NaN values.
842
+ x = x.strip()
843
+ return np.nan if x.lower() == "nan" else x
844
+
845
+ ncols = cols[-1][1] + 1
846
+ csv_kwargs["converters"] = {i: strip_field for i in range(ncols)}
847
+ """
848
+ try:
849
+ df = pandas.read_csv(fnamecsv, **csv_kwargs)
850
+ except:
851
+ error('Malformed response? Could not read response: {}'.format(urlcsv))
852
+
853
+ # Allocate output N-D array (It is not possible to pass dtype=dt
854
+ # as computed to pandas.read_csv; pandas dtype is different
855
+ # from numpy's dtype.)
856
+ data = np.ndarray(shape=(len(df)), dtype=dt)
857
+ # Insert data from dataframe 'df' columns into N-D array 'data'
858
+ for i in range(0, len(pnames)):
859
+ shape = np.append(len(data), psizes[i])
860
+ # In numpy 1.8.2 and Python 2.7, this throws an error
861
+ # for no apparent reason. Works as expected in numpy 1.10.4
862
+ data[pnames[i]] = np.squeeze(
863
+ np.reshape(df.values[:, np.arange(cols[i][0], cols[i][1] + 1)], shape))
864
+ else:
865
+ data = parse_missing_length(fnamecsv, dt, cols, psizes, pnames, ptypes, opts)
805
866
 
806
867
  toc = time.time() - tic
807
868
 
@@ -835,7 +896,14 @@ def hapi(*args, **kwargs):
835
896
  pickle.dump(meta, f, protocol=2)
836
897
 
837
898
  log('Writing %s' % fnamenpy, opts)
838
- np.save(fnamenpy, data)
899
+ with warnings.catch_warnings():
900
+ # Ignore warning that occurs when saving Unicode data.
901
+ warnings.filterwarnings("ignore",
902
+ message=r"Stored array in format 3\.0.*",
903
+ category=UserWarning,
904
+ module=r"numpy\.lib\.format",
905
+ )
906
+ np.save(fnamenpy, data)
839
907
 
840
908
  meta['x_totalTime'] = time.time() - tic_totalTime
841
909
 
@@ -933,9 +1001,28 @@ def parse_missing_length(fnamecsv, dt, cols, psizes, pnames, ptypes, opts):
933
1001
  if opts['method'] == 'numpy' or opts['method'] == 'numpynolength':
934
1002
  # If requested method was numpy, use numpynolength method.
935
1003
 
936
- # With dtype='None', the data type is determined automatically
937
- table = np.genfromtxt(fnamecsv, dtype=None, deletechars='',
938
- delimiter=',', encoding='utf-8')
1004
+ ncols = cols[-1][1] + 1
1005
+
1006
+ def normalize_field(value):
1007
+ if isinstance(value, bytes):
1008
+ value = value.decode('utf-8')
1009
+ value = value.strip()
1010
+ if len(value) >= 2 and value[0] == '"' and value[-1] == '"':
1011
+ value = value[1:-1].strip()
1012
+ if value.lower() == 'nan':
1013
+ return 'nan'
1014
+ return value
1015
+
1016
+ converters = {i: normalize_field for i in range(ncols)}
1017
+
1018
+ table = np.genfromtxt(fnamecsv,
1019
+ dtype=None,
1020
+ deletechars='',
1021
+ replace_space=' ',
1022
+ delimiter=',',
1023
+ encoding='utf-8',
1024
+ converters=converters)
1025
+
939
1026
  # table is a 1-D array. Each element is a row in the file.
940
1027
  # - If the data types are not the same for each column,
941
1028
  # the elements are tuples with length equal to the number
@@ -980,8 +1067,17 @@ def parse_missing_length(fnamecsv, dt, cols, psizes, pnames, ptypes, opts):
980
1067
  if opts['method'] == '' or opts['method'] == 'pandas' or opts['method'] == 'pandasnolength':
981
1068
  # If requested method was pandas, use pandasnolength method.
982
1069
 
1070
+ # TODO: Duplicate code.
983
1071
  # Read file into Pandas DataFrame
984
- df = pandas.read_csv(fnamecsv, sep=',', header=None, encoding='utf-8')
1072
+ csv_kwargs = {
1073
+ 'sep': ',',
1074
+ 'header': None,
1075
+ 'encoding': 'utf-8',
1076
+ 'skipinitialspace': True,
1077
+ 'keep_default_na': False,
1078
+ 'na_values': ['NaN', 'nan', 'Nan', 'naN', ' "NaN"', ' "nan"', ' "Nan"', ' "naN"', '"NaN"', '"nan"', '"Nan"', '"naN"']
1079
+ }
1080
+ df = pandas.read_csv(fnamecsv, **csv_kwargs)
985
1081
 
986
1082
  # Allocate output N-D array (It is not possible to pass dtype=dt
987
1083
  # as computed to pandas.read_csv, so need to create new ND array.)