diff --git a/README.md b/README.md index f378c1f..134c076 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,28 @@ -ecmwf-api-client -================ +# ecmwf-api-client -Installation -============ +# Installation Install via pip with: > $ pip install ecmwf-api-client -Configure -========= +# Configure -* If you don't have an ECMWF account, please self register at https://apps.ecmwf.int/registration/ and then go to the steps below. -* Login https://apps.ecmwf.int/auth/login/ -* Retrieve you key at https://api.ecmwf.int/v1/key/ +## Anonymous access (default, not recommended) - Note that the key expires in 1 year. You will receive an email to the registered email address 1 month before the expiration date with the renewal instructions. To check the expiry date of your current key log into www.ecmwf.int, and go to https://api.ecmwf.int/v1/key/. +Anonymous access is the default type of access, with no configuration needed. -* Copy the information in this page and paste it in the file $HOME/.ecmwfapirc (Unix/Linux) or %USERPROFILE%\\.ecmwfapirc (Windows: usually in C:\\Users\\\\\.ecmwfapirc ; see how to create a file with a leading dot). +However, anonymous access is only available for a limited set of datasets, and comes with a much lower quality of service. For access to all the available datasets, and an improved quality of service, please use registered access (see below). + +## Registered access (recommended) + +* Register with ECMWF at https://apps.ecmwf.int/registration/. +* Login at https://apps.ecmwf.int/auth/login/. +* Retrieve you API access key at https://api.ecmwf.int/v1/key/. + + Note that the API access key expires in 1 year. You will receive an email to the registered email address 1 month before the expiration date with the renewal instructions. To check the expiry date of your current key, log into www.ecmwf.int, and go to https://api.ecmwf.int/v1/key/. + +* Copy and paste the API access key into the file $HOME/.ecmwfapirc (Unix/Linux) or %USERPROFILE%\\.ecmwfapirc (Windows: usually in C:\\Users\\\\\.ecmwfapirc). Your $HOME/.ecmwfapirc (Unix/Linux) or %USERPROFILE%\\.ecmwfapirc (Windows) should look something like this: ``` @@ -27,12 +32,12 @@ Configure "email" : "john.smith@example.com" } ``` -* You can browse the ECMWF data catalogue at https://apps.ecmwf.int/mars-catalogue/ - -Example -======= +* Alternatively, one can use a file of their own liking, and point to it using environment variable `ECMWF_API_RC_FILE`. `ECMWF_API_RC_FILE` should be set to the full path of the given file. This method takes priority of the previous method of using a .ecmwfapirc file. +* As yet another option, one can set the API access key values directly in the environment using variables `ECMWF_API_KEY` (key), `ECMWF_API_URL` (url), `ECMWF_API_EMAIL` (email). This method takes priority over the previous method of using environment variable `ECMWF_API_RC_FILE`. + +# Example -You can test this small python script to retrieve TIGGE data: +You can test this small python script to retrieve TIGGE (https://apps.ecmwf.int/datasets/data/tigge) data. Note that access to TIGGE data requires registered access, and is subject to accepting a licence at https://apps.ecmwf.int/datasets/data/tigge/licence/. ``` #!/usr/bin/env python from ecmwfapi import ECMWFDataServer @@ -59,8 +64,7 @@ server.retrieve({ }) ``` -Logging -======= +# Logging Logging messages by default are emitted to `stdout` using Python's `print` statement. @@ -78,8 +82,7 @@ def my_logging_function(msg): server = ECMWFDataServer(log=my_logging_function) ``` -License -======= +# License Copyright 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 diff --git a/ecmwfapi/api.py b/ecmwfapi/api.py index e639289..d4ea10f 100755 --- a/ecmwfapi/api.py +++ b/ecmwfapi/api.py @@ -47,66 +47,113 @@ VERSION = "1.6.2" -class APIKeyFetchError(Exception): +DEFAULT_RCFILE_PATH = "~/.ecmwfapirc" +ANONYMOUS_APIKEY_VALUES = ( + "anonymous", + "https://api.ecmwf.int/v1", + "anonymous@ecmwf.int", +) + + +class APIKeyNotFoundError(Exception): pass -def _get_apikey_from_environ(): - try: - key = os.environ["ECMWF_API_KEY"] - url = os.environ["ECMWF_API_URL"] - email = os.environ["ECMWF_API_EMAIL"] - return key, url, email - except KeyError: - raise APIKeyFetchError("ERROR: Could not get the API key from the environment") +class APIKeyFetchError(Exception): + pass -def _get_apikey_from_rcfile(): - if "ECMWF_API_RC_FILE" in os.environ: - rc = os.path.normpath(os.path.expanduser(os.environ["ECMWF_API_RC_FILE"])) +def get_apikey_values_from_environ(): + apikey_values = ( + os.getenv("ECMWF_API_KEY"), + os.getenv("ECMWF_API_URL"), + os.getenv("ECMWF_API_EMAIL"), + ) + + if not any(apikey_values): + raise APIKeyNotFoundError("ERROR: No API key found in the environment") + elif not all(apikey_values): + raise APIKeyFetchError("ERROR: Incomplete API key found in the environment") else: - rc = os.path.normpath(os.path.expanduser("~/.ecmwfapirc")) + return apikey_values + + +def get_apikey_values_from_rcfile(rcfile_path): + rcfile_path = os.path.normpath(os.path.expanduser(rcfile_path)) try: - with open(rc) as f: - config = json.load(f) + with open(rcfile_path) as f: + apikey = json.load(f) + except FileNotFoundError as e: + raise APIKeyNotFoundError(str(e)) except IOError as e: # Failed reading from file raise APIKeyFetchError(str(e)) except ValueError: # JSON decoding failed - raise APIKeyFetchError("ERROR: Missing or malformed API key in '%s'" % rc) + raise APIKeyFetchError( + "ERROR: Missing or malformed API key in '%s'" % rcfile_path + ) except Exception as e: # Unexpected error raise APIKeyFetchError(str(e)) - - try: - key = config["key"] - url = config["url"] - email = config["email"] - return key, url, email - except: - raise APIKeyFetchError("ERROR: Missing or malformed API key in '%s'" % rc) + else: + try: + return (apikey["key"], apikey["url"], apikey["email"]) + except: + raise APIKeyFetchError( + "ERROR: Missing or malformed API key in '%s'" % rcfile_path + ) def get_apikey_values(): - """Get the API key from either the environment or the '.ecmwfapirc' file, - in this order. If the API key is not available or invalid, use the API key - for anonymous access as a fallback. + """Get the API key values in Python tuple format either directly from the + environment, or from a file. If no API key is found, fall back to anonymous + access. + + The complete workflow is the following: + + - Step 1: the environment is checked for variables ECMWF_API_KEY, + ECMWF_API_URL, ECMWF_API_EMAIL. + * If all found, and not empty, return their values in Python tuple + format. + * If only some found, and not empty, assume an incomplete API key, and + raise APIKeyFetchError. + * If none found, or found but empty, assume no API key available in the + environment, and continue to the next step. + + - Step 2: the environment is checked for variable ECMWF_API_RC_FILE, meant + to point to a user defined API key file. + * If found, but pointing to a file not found, raise APIKeyNotFoundError. + * If found, and the file it points to exists, but cannot not be read, or + contains an invalid API key, raise APIKeyFetchError. + * If found, and the file it points to exists, can be read, and contains + a valid API key, return the API key in Python tuple format. + * If not found, or empty, assume no user provided API key file and + continue to the next step. + + - Step 3: try the default ~/.ecmwfapirc file. + * Same as step 2, except for when ~/.ecmwfapirc is not found, where we + continue to the next step. + + - Step 4: No API key found, so fall back to anonymous access. Returns: - Tuple with the API key token, url, and email. + Pyhon tuple with the API key token, url, and email. + + Raises: + APIKeyFetchError: If an API key is found, but invalid. + APIKeyNotFound: When ECMWF_API_RC_FILE is defined, but pointing to a + file that does not exist. """ try: - key_values = _get_apikey_from_environ() - except APIKeyFetchError: - try: - key_values = _get_apikey_from_rcfile() - except APIKeyFetchError: - return ( - os.environ.get("ECMWF_API_KEY", "anonymous"), - os.environ.get("ECMWF_API_URL", "https://api.ecmwf.int/v1"), - os.environ.get("ECMWF_API_EMAIL", "anonymous@ecmwf.int"), - ) - - return key_values + return get_apikey_values_from_environ() + except APIKeyNotFoundError: + env_rcfile_path = os.getenv("ECMWF_API_RC_FILE") + if env_rcfile_path: + return get_apikey_values_from_rcfile(env_rcfile_path) + else: + try: + return get_apikey_values_from_rcfile(DEFAULT_RCFILE_PATH) + except APIKeyNotFoundError: + return ANONYMOUS_APIKEY_VALUES class RetryError(Exception): @@ -399,7 +446,7 @@ def __init__( user = self.connection.call("%s/%s" % (self.url, "who-am-i")) - if os.environ.get("GITHUB_ACTION") is None: + if os.getenv("GITHUB_ACTION") is None: self.log("Welcome %s" % (user["full_name"] or "user '%s'" % user["uid"],)) general_info = self.connection.call("%s/%s" % (self.url, "info")).get("info")