-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fail fast when an invalid API access key is provided (#19)
- Loading branch information
1 parent
53a70ef
commit 4f30c35
Showing
2 changed files
with
111 additions
and
61 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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\\\<USERNAME\>\\.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\\\<USERNAME\>\\.ecmwfapirc). | ||
|
||
Your $HOME/.ecmwfapirc (Unix/Linux) or %USERPROFILE%\\.ecmwfapirc (Windows) should look something like this: | ||
``` | ||
|
@@ -27,12 +32,12 @@ Configure | |
"email" : "[email protected]" | ||
} | ||
``` | ||
* 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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -47,66 +47,113 @@ | |
VERSION = "1.6.2" | ||
|
||
|
||
class APIKeyFetchError(Exception): | ||
DEFAULT_RCFILE_PATH = "~/.ecmwfapirc" | ||
ANONYMOUS_APIKEY_VALUES = ( | ||
"anonymous", | ||
"https://api.ecmwf.int/v1", | ||
"[email protected]", | ||
) | ||
|
||
|
||
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", "[email protected]"), | ||
) | ||
|
||
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") | ||
|