summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGard Spreemann <gspr@nonempty.org>2019-05-23 13:45:38 +0200
committerGard Spreemann <gspr@nonempty.org>2019-05-23 13:45:38 +0200
commitf15a4382091aae707a95302ee5a3ad5bafc724bc (patch)
tree8a8f2461f51e498a6b9fc1b7d21a2fa12c3ee562
parenta025b6c38c32e4a580c6f521cee277a64d04fc12 (diff)
parent11e00a4a0a3771097ddddcdb8fa3c417407adf3b (diff)
Merge tag 'upstream/0.1.4' into debian/latest
Upstream 0.1.4 tarball as released on PyPI.
-rw-r--r--CONTRIBUTING.rst51
-rw-r--r--LICENSE.txt177
-rw-r--r--MANIFEST.in10
-rw-r--r--PKG-INFO84
-rw-r--r--README.rst62
-rw-r--r--cdsapi.egg-info/PKG-INFO84
-rw-r--r--cdsapi.egg-info/SOURCES.txt21
-rw-r--r--cdsapi.egg-info/dependency_links.txt1
-rw-r--r--cdsapi.egg-info/requires.txt1
-rw-r--r--cdsapi.egg-info/top_level.txt1
-rw-r--r--cdsapi.egg-info/zip-safe1
-rw-r--r--cdsapi/__init__.py23
-rw-r--r--cdsapi/api.py379
-rwxr-xr-xexample-era5.py26
-rwxr-xr-xexample-glaciers.py22
-rw-r--r--setup.cfg22
-rw-r--r--setup.py65
-rw-r--r--tests/requirements-dev.txt7
-rw-r--r--tests/requirements-tests.in7
-rw-r--r--tests/requirements-tests.txt9
-rw-r--r--tests/test_api.py13
-rw-r--r--tox.ini16
22 files changed, 1082 insertions, 0 deletions
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
new file mode 100644
index 0000000..d750f33
--- /dev/null
+++ b/CONTRIBUTING.rst
@@ -0,0 +1,51 @@
+
+.. highlight:: console
+
+How to develop
+--------------
+
+Install the package following README.rst and then install development dependencies::
+
+ $ pip install -U -r tests/requirements-dev.txt
+
+Unit tests can be run with `pytest <https://pytest.org>`_ with::
+
+ $ pytest -v --flakes --cov=cdsapi --cov-report=html --cache-clear
+
+Coverage can be checked opening in a browser the file ``htmlcov/index.html`` for example with::
+
+ $ open htmlcov/index.html
+
+Code quality control checks can be run with::
+
+ $ pytest -v --pep8 --mccabe
+
+The complete python versions tests are run via `tox <https://tox.readthedocs.io>`_ with::
+
+ $ tox
+
+Please ensure the coverage at least stays the same before you submit a pull request.
+
+
+Dependency management
+---------------------
+
+Update the `requirements-tests.txt` file with versions with::
+
+ pip-compile -U -o tests/requirements-tests.txt setup.py tests/requirements-tests.in # -U is optional
+
+
+Release procedure
+-----------------
+
+Quality check release::
+
+ $ git status
+ $ check-manifest
+ $ tox
+
+Release with zest.releaser::
+
+ $ prerelease
+ $ release
+ $ postrelease
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..f433b1a
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,177 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..e95932d
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,10 @@
+include *.in
+include *.rst
+include *.txt
+include *.py
+include LICENSE
+include tox.ini
+recursive-include cdsapi *.py
+recursive-include tests *.in
+recursive-include tests *.py
+recursive-include tests *.txt
diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644
index 0000000..bb3309d
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,84 @@
+Metadata-Version: 1.1
+Name: cdsapi
+Version: 0.1.4
+Summary: Climate Data Store API
+Home-page: https://software.ecmwf.int/stash/projects/CDS/repos/cdsapi
+Author: ECMWF
+Author-email: software.support@ecmwf.int
+License: Apache 2.0
+Description:
+ Install
+ -------
+
+ Install via `pip` with::
+
+ $ pip install cdsapi
+
+
+ Configure
+ ---------
+
+ Get your UID and API key from the CDS portal at the address https://cds.climate.copernicus.eu/user
+ and write it into the configuration file, so it looks like::
+
+ $ cat ~/.cdsapirc
+ url: https://cds.climate.copernicus.eu/api/v2
+ key: <UID>:<API key>
+ verify: 0
+
+ Remember to agree to the Terms and Conditions of every dataset that you intend to download.
+
+
+ Test
+ ----
+
+ Perform a small test retrieve of ERA5 data::
+
+ $ python
+ >>> import cdsapi
+ >>> cds = cdsapi.Client()
+ >>> cds.retrieve('reanalysis-era5-pressure-levels', {
+ "variable": "temperature",
+ "pressure_level": "1000",
+ "product_type": "reanalysis",
+ "date": "2017-12-01/2017-12-31",
+ "time": "12:00",
+ "format": "grib"
+ }, 'download.grib')
+ >>>
+
+
+ License
+ -------
+
+ Copyright 2018 - 2019 European Centre for Medium-Range Weather Forecasts (ECMWF)
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ In applying this licence, ECMWF does not waive the privileges and immunities
+ granted to it by virtue of its status as an intergovernmental organisation nor
+ does it submit to any jurisdiction.
+
+Platform: UNKNOWN
+Classifier: Development Status :: 3 - Alpha
+Classifier: Intended Audience :: Developers
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Operating System :: OS Independent
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..166dac0
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,62 @@
+
+Install
+-------
+
+Install via `pip` with::
+
+ $ pip install cdsapi
+
+
+Configure
+---------
+
+Get your UID and API key from the CDS portal at the address https://cds.climate.copernicus.eu/user
+and write it into the configuration file, so it looks like::
+
+ $ cat ~/.cdsapirc
+ url: https://cds.climate.copernicus.eu/api/v2
+ key: <UID>:<API key>
+ verify: 0
+
+Remember to agree to the Terms and Conditions of every dataset that you intend to download.
+
+
+Test
+----
+
+Perform a small test retrieve of ERA5 data::
+
+ $ python
+ >>> import cdsapi
+ >>> cds = cdsapi.Client()
+ >>> cds.retrieve('reanalysis-era5-pressure-levels', {
+ "variable": "temperature",
+ "pressure_level": "1000",
+ "product_type": "reanalysis",
+ "date": "2017-12-01/2017-12-31",
+ "time": "12:00",
+ "format": "grib"
+ }, 'download.grib')
+ >>>
+
+
+License
+-------
+
+Copyright 2018 - 2019 European Centre for Medium-Range Weather Forecasts (ECMWF)
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+In applying this licence, ECMWF does not waive the privileges and immunities
+granted to it by virtue of its status as an intergovernmental organisation nor
+does it submit to any jurisdiction.
diff --git a/cdsapi.egg-info/PKG-INFO b/cdsapi.egg-info/PKG-INFO
new file mode 100644
index 0000000..bb3309d
--- /dev/null
+++ b/cdsapi.egg-info/PKG-INFO
@@ -0,0 +1,84 @@
+Metadata-Version: 1.1
+Name: cdsapi
+Version: 0.1.4
+Summary: Climate Data Store API
+Home-page: https://software.ecmwf.int/stash/projects/CDS/repos/cdsapi
+Author: ECMWF
+Author-email: software.support@ecmwf.int
+License: Apache 2.0
+Description:
+ Install
+ -------
+
+ Install via `pip` with::
+
+ $ pip install cdsapi
+
+
+ Configure
+ ---------
+
+ Get your UID and API key from the CDS portal at the address https://cds.climate.copernicus.eu/user
+ and write it into the configuration file, so it looks like::
+
+ $ cat ~/.cdsapirc
+ url: https://cds.climate.copernicus.eu/api/v2
+ key: <UID>:<API key>
+ verify: 0
+
+ Remember to agree to the Terms and Conditions of every dataset that you intend to download.
+
+
+ Test
+ ----
+
+ Perform a small test retrieve of ERA5 data::
+
+ $ python
+ >>> import cdsapi
+ >>> cds = cdsapi.Client()
+ >>> cds.retrieve('reanalysis-era5-pressure-levels', {
+ "variable": "temperature",
+ "pressure_level": "1000",
+ "product_type": "reanalysis",
+ "date": "2017-12-01/2017-12-31",
+ "time": "12:00",
+ "format": "grib"
+ }, 'download.grib')
+ >>>
+
+
+ License
+ -------
+
+ Copyright 2018 - 2019 European Centre for Medium-Range Weather Forecasts (ECMWF)
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ In applying this licence, ECMWF does not waive the privileges and immunities
+ granted to it by virtue of its status as an intergovernmental organisation nor
+ does it submit to any jurisdiction.
+
+Platform: UNKNOWN
+Classifier: Development Status :: 3 - Alpha
+Classifier: Intended Audience :: Developers
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Operating System :: OS Independent
diff --git a/cdsapi.egg-info/SOURCES.txt b/cdsapi.egg-info/SOURCES.txt
new file mode 100644
index 0000000..cbd1fce
--- /dev/null
+++ b/cdsapi.egg-info/SOURCES.txt
@@ -0,0 +1,21 @@
+CONTRIBUTING.rst
+LICENSE.txt
+MANIFEST.in
+README.rst
+example-era5.py
+example-glaciers.py
+setup.cfg
+setup.py
+tox.ini
+cdsapi/__init__.py
+cdsapi/api.py
+cdsapi.egg-info/PKG-INFO
+cdsapi.egg-info/SOURCES.txt
+cdsapi.egg-info/dependency_links.txt
+cdsapi.egg-info/requires.txt
+cdsapi.egg-info/top_level.txt
+cdsapi.egg-info/zip-safe
+tests/requirements-dev.txt
+tests/requirements-tests.in
+tests/requirements-tests.txt
+tests/test_api.py \ No newline at end of file
diff --git a/cdsapi.egg-info/dependency_links.txt b/cdsapi.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/cdsapi.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/cdsapi.egg-info/requires.txt b/cdsapi.egg-info/requires.txt
new file mode 100644
index 0000000..8f94722
--- /dev/null
+++ b/cdsapi.egg-info/requires.txt
@@ -0,0 +1 @@
+requests>=2.5.0
diff --git a/cdsapi.egg-info/top_level.txt b/cdsapi.egg-info/top_level.txt
new file mode 100644
index 0000000..e9998de
--- /dev/null
+++ b/cdsapi.egg-info/top_level.txt
@@ -0,0 +1 @@
+cdsapi
diff --git a/cdsapi.egg-info/zip-safe b/cdsapi.egg-info/zip-safe
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/cdsapi.egg-info/zip-safe
@@ -0,0 +1 @@
+
diff --git a/cdsapi/__init__.py b/cdsapi/__init__.py
new file mode 100644
index 0000000..c9e9d4e
--- /dev/null
+++ b/cdsapi/__init__.py
@@ -0,0 +1,23 @@
+# Copyright 2018 European Centre for Medium-Range Weather Forecasts (ECMWF)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# In applying this licence, ECMWF does not waive the privileges and immunities
+# granted to it by virtue of its status as an intergovernmental organisation nor
+# does it submit to any jurisdiction.
+
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+from . import api
+
+Client = api.Client
diff --git a/cdsapi/api.py b/cdsapi/api.py
new file mode 100644
index 0000000..88a3a8c
--- /dev/null
+++ b/cdsapi/api.py
@@ -0,0 +1,379 @@
+# (C) Copyright 2018 ECMWF.
+#
+# This software is licensed under the terms of the Apache Licence Version 2.0
+# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
+# In applying this licence, ECMWF does not waive the privileges and immunities
+# granted to it by virtue of its status as an intergovernmental organisation nor
+# does it submit to any jurisdiction.
+
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+import json
+import time
+import os
+import logging
+
+import requests
+
+
+def bytes_to_string(n):
+ u = ['', 'K', 'M', 'G', 'T', 'P']
+ i = 0
+ while n >= 1024:
+ n /= 1024.0
+ i += 1
+ return '%g%s' % (int(n * 10 + 0.5) / 10.0, u[i])
+
+
+def read_config(path):
+ config = {}
+ with open(path) as f:
+ for l in f.readlines():
+ if ':' in l:
+ k, v = l.strip().split(':', 1)
+ if k in ('url', 'key', 'verify'):
+ config[k] = v.strip()
+ return config
+
+
+class Result(object):
+
+ def __init__(self, client, reply):
+
+ self.reply = reply
+
+ self._url = client.url
+
+ self.session = client.session
+ self.robust = client.robust
+ self.verify = client.verify
+ self.cleanup = client.delete
+
+ self.debug = client.debug
+ self.info = client.info
+ self.warning = client.warning
+ self.error = client.error
+
+ self._deleted = False
+
+ def _download(self, url, size, target):
+
+ if target is None:
+ target = url.split('/')[-1]
+
+ self.info("Downloading %s to %s (%s)", url, target, bytes_to_string(size))
+ start = time.time()
+
+ r = self.robust(requests.get)(url, stream=True, verify=self.verify)
+ try:
+ r.raise_for_status()
+
+ total = 0
+ with open(target, 'wb') as f:
+ for chunk in r.iter_content(chunk_size=1024):
+ if chunk:
+ f.write(chunk)
+ total += len(chunk)
+ finally:
+ r.close()
+
+ if total != size:
+ raise Exception("Download failed: downloaded %s byte(s) out of %s" % (total, size))
+
+ elapsed = time.time() - start
+ if elapsed:
+ self.info("Download rate %s/s", bytes_to_string(size / elapsed))
+
+ return target
+
+ def download(self, target=None):
+ return self._download(self.location,
+ self.content_length,
+ target)
+
+ @property
+ def content_length(self):
+ return int(self.reply['content_length'])
+
+ @property
+ def location(self):
+ return self.reply['location']
+
+ @property
+ def content_type(self):
+ return self.reply['content_type']
+
+ def __repr__(self):
+ return "Result(content_length=%s,content_type=%s,location=%s)" % (self.content_length,
+ self.content_type,
+ self.location)
+
+ def check(self):
+ self.debug("HEAD %s", self.reply['location'])
+ metadata = self.robust(self.session.head)(self.reply['location'],
+ verify=self.verify)
+ metadata.raise_for_status()
+ self.debug(metadata.headers)
+ return metadata
+
+ def delete(self):
+
+ if self._deleted:
+ return
+
+ if 'request_id' in self.reply:
+ rid = self.reply['request_id']
+
+ task_url = '%s/tasks/%s' % (self._url, rid)
+ self.debug("DELETE %s", task_url)
+
+ delete = self.session.delete(task_url, verify=self.verify)
+ self.debug("DELETE returns %s %s", delete.status_code, delete.reason)
+
+ try:
+ delete.raise_for_status()
+ except Exception:
+ self.warning("DELETE %s returns %s %s",
+ task_url, delete.status_code, delete.reason)
+
+ self._deleted = True
+
+ def __del__(self):
+ try:
+ if self.cleanup:
+ self.delete()
+ except Exception as e:
+ print(e)
+
+
+class Client(object):
+
+ logger = logging.getLogger('cdsapi')
+
+ def __init__(self,
+ url=os.environ.get('CDSAPI_URL'),
+ key=os.environ.get('CDSAPI_KEY'),
+ quiet=False,
+ debug=False,
+ verify=None,
+ timeout=None,
+ full_stack=False,
+ delete=True,
+ retry_max=500,
+ sleep_max=120,
+ info_callback=None,
+ warning_callback=None,
+ error_callback=None,
+ debug_callback=None,
+ ):
+
+ if not quiet:
+
+ if debug:
+ level = logging.DEBUG
+ else:
+ level = logging.INFO
+
+ logging.basicConfig(level=level,
+ format='%(asctime)s %(levelname)s %(message)s')
+
+ dotrc = os.environ.get('CDSAPI_RC', os.path.expanduser('~/.cdsapirc'))
+
+ if url is None or key is None:
+ if os.path.exists(dotrc):
+ config = read_config(dotrc)
+
+ if key is None:
+ key = config.get('key')
+
+ if url is None:
+ url = config.get('url')
+
+ if verify is None:
+ verify = int(config.get('verify', 1))
+
+ if url is None or key is None or key is None:
+ raise Exception('Missing/incomplete configuration file: %s' % (dotrc))
+
+ self.url = url
+ self.key = key
+
+ self.quiet = quiet
+ self.verify = True if verify else False
+ self.timeout = timeout
+ self.sleep_max = sleep_max
+ self.retry_max = retry_max
+ self.full_stack = full_stack
+ self.delete = delete
+ self.last_state = None
+
+ self.debug_callback = debug_callback
+ self.warning_callback = warning_callback
+ self.info_callback = info_callback
+ self.error_callback = error_callback
+
+ self.session = requests.Session()
+ self.session.auth = tuple(self.key.split(':', 2))
+
+ self.debug("CDSAPI %s", dict(url=self.url,
+ key=self.key,
+ quiet=self.quiet,
+ verify=self.verify,
+ timeout=self.timeout,
+ sleep_max=self.sleep_max,
+ retry_max=self.retry_max,
+ full_stack=self.full_stack,
+ delete=self.delete
+ ))
+
+ def retrieve(self, name, request, target=None):
+ result = self._api('%s/resources/%s' % (self.url, name), request)
+ if target is not None:
+ result.download(target)
+ return result
+
+ def identity(self):
+ return self._api('%s/resources' % (self.url,), {})
+
+ def _api(self, url, request):
+
+ session = self.session
+
+ self.info("Sending request to %s", url)
+ self.debug("POST %s %s", url, json.dumps(request))
+
+ result = self.robust(session.post)(url, json=request, verify=self.verify)
+ reply = None
+
+ try:
+ result.raise_for_status()
+ reply = result.json()
+ except Exception:
+
+ if reply is None:
+ try:
+ reply = result.json()
+ except Exception:
+ reply = dict(message=result.text)
+
+ self.debug(json.dumps(reply))
+
+ if 'message' in reply:
+ error = reply['message']
+
+ if 'context' in reply and 'required_terms' in reply['context']:
+ e = [error]
+ for t in reply['context']['required_terms']:
+ e.append("To access this resource, you first need to accept the terms"
+ "of '%s' at %s" % (t['title'], t['url']))
+ error = '. '.join(e)
+ raise Exception(error)
+ else:
+ raise
+
+ sleep = 1
+ start = time.time()
+
+ while True:
+
+ self.debug("REPLY %s", reply)
+
+ if reply['state'] != self.last_state:
+ self.info("Request is %s" % (reply['state'],))
+ self.last_state = reply['state']
+
+ if reply['state'] == 'completed':
+ self.debug("Done")
+ return Result(self, reply)
+
+ if reply['state'] in ('queued', 'running'):
+ rid = reply['request_id']
+
+ if self.timeout and (time.time() - start > self.timeout):
+ raise Exception('TIMEOUT')
+
+ self.debug("Request ID is %s, sleep %s", rid, sleep)
+ time.sleep(sleep)
+ sleep *= 1.5
+ if sleep > self.sleep_max:
+ sleep = self.sleep_max
+
+ task_url = '%s/tasks/%s' % (self.url, rid)
+ self.debug("GET %s", task_url)
+
+ result = self.robust(session.get)(task_url, verify=self.verify)
+ result.raise_for_status()
+ reply = result.json()
+ continue
+
+ if reply['state'] in ('failed',):
+ self.error("Message: %s", reply['error'].get('message'))
+ self.error("Reason: %s", reply['error'].get('reason'))
+ for n in reply.get('error', {}).get('context', {}).get('traceback', '').split('\n'):
+ if n.strip() == '' and not self.full_stack:
+ break
+ self.error(" %s", n)
+ raise Exception("%s. %s." % (reply['error'].get('message'), reply['error'].get('reason')))
+
+ raise Exception('Unknown API state [%s]' % (reply['state'],))
+
+ def info(self, *args, **kwargs):
+ if self.info_callback:
+ self.info_callback(*args, **kwargs)
+ else:
+ self.logger.info(*args, **kwargs)
+
+ def warning(self, *args, **kwargs):
+ if self.warning_callback:
+ self.warning_callback(*args, **kwargs)
+ else:
+ self.logger.warning(*args, **kwargs)
+
+ def error(self, *args, **kwargs):
+ if self.error_callback:
+ self.error_callback(*args, **kwargs)
+ else:
+ self.logger.error(*args, **kwargs)
+
+ def debug(self, *args, **kwargs):
+ if self.debug_callback:
+ self.debug_callback(*args, **kwargs)
+ else:
+ self.logger.debug(*args, **kwargs)
+
+ def robust(self, call):
+
+ def retriable(code, reason):
+
+ if code in [requests.codes.internal_server_error,
+ requests.codes.bad_gateway,
+ requests.codes.service_unavailable,
+ requests.codes.gateway_timeout,
+ requests.codes.too_many_requests,
+ requests.codes.request_timeout]:
+ return True
+
+ return False
+
+ def wrapped(*args, **kwargs):
+ tries = 0
+ while tries < self.retry_max:
+ try:
+ r = call(*args, **kwargs)
+ except requests.exceptions.ConnectionError as e:
+ r = None
+ self.warning("Recovering from connection error [%s], attemps %s of %s",
+ e, tries, self.retry_max)
+
+ if r is not None:
+ if not retriable(r.status_code, r.reason):
+ return r
+ self.warning("Recovering from HTTP error [%s %s], attemps %s of %s",
+ r.status_code, r.reason, tries, self.retry_max)
+
+ tries += 1
+
+ self.warning("Retrying in %s seconds", self.sleep_max)
+ time.sleep(self.sleep_max)
+
+ return wrapped
diff --git a/example-era5.py b/example-era5.py
new file mode 100755
index 0000000..c0ad58b
--- /dev/null
+++ b/example-era5.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python
+
+# (C) Copyright 2018 ECMWF.
+#
+# This software is licensed under the terms of the Apache Licence Version 2.0
+# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
+# In applying this licence, ECMWF does not waive the privileges and immunities
+# granted to it by virtue of its status as an intergovernmental organisation nor
+# does it submit to any jurisdiction.
+
+import cdsapi
+
+c = cdsapi.Client(debug=True)
+
+r = c.retrieve(
+ "reanalysis-era5-single-levels",
+ {
+ "variable": "2t",
+ "product_type": "reanalysis",
+ "date": "2012-12-01",
+ "time": "14:00",
+ "format": "netcdf",
+ },
+)
+
+r.download("test.nc")
diff --git a/example-glaciers.py b/example-glaciers.py
new file mode 100755
index 0000000..00f0709
--- /dev/null
+++ b/example-glaciers.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+
+# (C) Copyright 2018 ECMWF.
+#
+# This software is licensed under the terms of the Apache Licence Version 2.0
+# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
+# In applying this licence, ECMWF does not waive the privileges and immunities
+# granted to it by virtue of its status as an intergovernmental organisation nor
+# does it submit to any jurisdiction.
+
+import cdsapi
+
+c = cdsapi.Client()
+
+c.retrieve(
+ "insitu-glaciers-elevation-mass",
+ {
+ "variable": "elevation_change",
+ "format": "tgz"
+ },
+ "dowload.data",
+)
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..fcab8c2
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,22 @@
+[bdist_wheel]
+universal = 1
+
+[aliases]
+test = pytest
+
+[tool:pytest]
+norecursedirs =
+ build
+ dist
+ .tox
+ .eggs
+pep8maxlinelength = 109
+mccabe-complexity = 10
+
+[coverage:run]
+branch = True
+
+[egg_info]
+tag_build =
+tag_date = 0
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..041cdcc
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,65 @@
+#!/usr/bin/env python3
+
+# Copyright 2018 European Centre for Medium-Range Weather Forecasts (ECMWF)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# In applying this licence, ECMWF does not waive the privileges and immunities
+# granted to it by virtue of its status as an intergovernmental organisation nor
+# does it submit to any jurisdiction.
+
+
+import io
+import os.path
+
+import setuptools
+
+
+def read(fname):
+ file_path = os.path.join(os.path.dirname(__file__), fname)
+ return io.open(file_path, encoding='utf-8').read()
+
+
+version = '0.1.4'
+
+
+setuptools.setup(
+ name='cdsapi',
+ version=version,
+ author='ECMWF',
+ author_email='software.support@ecmwf.int',
+ license='Apache 2.0',
+ url='https://software.ecmwf.int/stash/projects/CDS/repos/cdsapi',
+ description="Climate Data Store API",
+ long_description=read('README.rst'),
+ packages=setuptools.find_packages(),
+ include_package_data=True,
+ install_requires=[
+ 'requests>=2.5.0',
+ ],
+ zip_safe=True,
+ classifiers=[
+ 'Development Status :: 3 - Alpha',
+ 'Intended Audience :: Developers',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.4',
+ 'Programming Language :: Python :: 3.5',
+ 'Programming Language :: Python :: 3.6',
+ 'Programming Language :: Python :: Implementation :: CPython',
+ 'Programming Language :: Python :: Implementation :: PyPy',
+ 'Operating System :: OS Independent',
+ ],
+)
diff --git a/tests/requirements-dev.txt b/tests/requirements-dev.txt
new file mode 100644
index 0000000..049cc41
--- /dev/null
+++ b/tests/requirements-dev.txt
@@ -0,0 +1,7 @@
+check-manifest
+detox
+pip-tools
+pyroma
+tox
+tox-pyenv
+zest.releaser
diff --git a/tests/requirements-tests.in b/tests/requirements-tests.in
new file mode 100644
index 0000000..1017996
--- /dev/null
+++ b/tests/requirements-tests.in
@@ -0,0 +1,7 @@
+pytest
+pytest-flakes
+pytest-cov
+pytest-env
+pytest-mccabe
+pytest-pep8
+pytest-runner
diff --git a/tests/requirements-tests.txt b/tests/requirements-tests.txt
new file mode 100644
index 0000000..ad28cc6
--- /dev/null
+++ b/tests/requirements-tests.txt
@@ -0,0 +1,9 @@
+#
+pytest-cov
+pytest-env
+pytest-flakes
+pytest-mccabe
+pytest-pep8
+pytest-runner
+pytest
+requests
diff --git a/tests/test_api.py b/tests/test_api.py
new file mode 100644
index 0000000..2403dca
--- /dev/null
+++ b/tests/test_api.py
@@ -0,0 +1,13 @@
+
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+from cdsapi import api
+
+
+def test_bytes_to_string():
+ assert api.bytes_to_string(1) == '1'
+ assert api.bytes_to_string(1 << 10) == '1K'
+ assert api.bytes_to_string(1 << 20) == '1M'
+ assert api.bytes_to_string(1 << 30) == '1G'
+ assert api.bytes_to_string(1 << 40) == '1T'
+ assert api.bytes_to_string(1 << 50) == '1P'
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..1d50849
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,16 @@
+[tox]
+envlist = qc, py36, py35, py34, py27, pypy3, pypy, deps
+
+[testenv]
+setenv = PYTHONPATH = {toxinidir}
+deps = -r{toxinidir}/tests/requirements-tests.txt
+commands = pytest -v --flakes --cache-clear --basetemp={envtmpdir} {posargs}
+
+[testenv:qc]
+# needed for pytest-cov
+usedevelop = true
+commands = pytest -v --pep8 --mccabe --cov=cdsapi --cov-report=html --cache-clear {posargs}
+
+[testenv:deps]
+deps =
+commands = python setup.py test