Skip to content

Commit

Permalink
Fail fast when an invalid API access key is provided (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
cristian-codorean authored Jan 31, 2022
1 parent 53a70ef commit 4f30c35
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 61 deletions.
43 changes: 23 additions & 20 deletions README.md
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:
```
Expand All @@ -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
Expand All @@ -59,8 +64,7 @@ server.retrieve({
})
```

Logging
=======
# Logging

Logging messages by default are emitted to `stdout` using Python's `print` statement.

Expand All @@ -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
Expand Down
129 changes: 88 additions & 41 deletions ecmwfapi/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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")
Expand Down

0 comments on commit 4f30c35

Please sign in to comment.