diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bc8d2491..655ef4b4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,16 +7,15 @@ and this project adheres to [PEP 440](https://www.python.org/dev/peps/pep-0440/) and uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] -### Added -* [656](https://github.com/dbekaert/RAiDER/pull/656) - Example run configuration files available through `raider.py --generate_config `. ### Changed -* [651](https://github.com/dbekaert/RAiDER/pull/651) - Removed use of deprecated argument to `pandas.read_csv`. -* [662](https://github.com/dbekaert/RAiDER/pull/662) - Bumped dem-stitcher to >= v2.5.6, which updates the URL for reading the Geoid EGM 2008. -### Fixed * [627](https://github.com/dbekaert/RAiDER/pull/627) - Made Python datetimes timezone-aware and added unit tests and bug fixes. +* [651](https://github.com/dbekaert/RAiDER/pull/651) - Removed use of deprecated argument to `pandas.read_csv`. +* [652](https://github.com/dbekaert/RAiDER/pull/652) - Changed the behavior of `RAiDER.models.credentials.check_api` to not overwrite the user's API credential files. +* [656](https://github.com/dbekaert/RAiDER/pull/656) - Example run configuration files available through `raider.py --generate_config `. * [657](https://github.com/dbekaert/RAiDER/pull/657) - Fixed a few typos in `README.md`. * [658](https://github.com/dbekaert/RAiDER/pull/658) - Fixed opaque error message if a GUNW file is not produced while HyP3 independently against a previous INSAR_ISCE. * [661](https://github.com/dbekaert/RAiDER/pull/661) - Fixed bug in raiderDownloadGNSS, removed call to scipy.sum, and added unit tests. +* [662](https://github.com/dbekaert/RAiDER/pull/662) - Bumped dem-stitcher to >= v2.5.6, which updates the URL for reading the Geoid EGM 2008. ## [0.5.1] ### Changed diff --git a/test/__init__.py b/test/__init__.py index 3b6683a22..a948eaff4 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -2,6 +2,8 @@ import pytest import subprocess import shutil +import string +import random from contextlib import contextmanager from pathlib import Path @@ -82,4 +84,11 @@ def makeLatLonGrid(bbox, reg, out_dir, spacing=0.1): def make_delay_name(weather_model_name, date, time, kind='ztd'): assert kind in 'ztd std ray'.split(), 'Incorrect type of delays.' - return f'{weather_model_name}_tropo_{date}T{time.replace(":", "")}_{kind}.nc' \ No newline at end of file + return f'{weather_model_name}_tropo_{date}T{time.replace(":", "")}_{kind}.nc' + + +def random_string( + length: int = 32, + alphabet: str = string.ascii_letters + string.digits +) -> str: + return ''.join(random.choices(alphabet, k=length)) diff --git a/test/credentials/test_createFile.py b/test/credentials/test_createFile.py new file mode 100644 index 000000000..57f539e20 --- /dev/null +++ b/test/credentials/test_createFile.py @@ -0,0 +1,75 @@ +''' +When update_rc_file is either True or False, the relevant API RC file should be +created if it doesn't exist. +''' +from typing import Tuple + +import pytest +import os +from pathlib import Path +from platform import system +from RAiDER.models import credentials +from test import random_string + + +def get_creds_cds(rc_path: Path) -> Tuple[str, str]: + import cdsapi + cds_credentials = cdsapi.api.read_config(rc_path) + uid, key = cds_credentials['key'].split(':') + return uid, key + + +def get_creds_ecmwf(rc_path: Path) -> Tuple[str, str]: + import ecmwfapi + # Get current ECMWF API RC file path + old_rc_path = os.getenv("ECMWF_API_RC_FILE", ecmwfapi.api.DEFAULT_RCFILE_PATH) + + # Point ecmwfapi to current dir to avoid overwriting ~/.ecmwfapirc + os.environ["ECMWF_API_RC_FILE"] = str(rc_path) + key, _, uid = ecmwfapi.api.get_apikey_values() + + # Point ecmwfapi back to previous value and remove local API file + os.environ["ECMWF_API_RC_FILE"] = old_rc_path + return uid, key + + +def get_creds_netrc(rc_path: Path) -> Tuple[str, str]: + import netrc + host = 'urs.earthdata.nasa.gov' + netrc_credentials = netrc.netrc(rc_path) + uid, _, key = netrc_credentials.authenticators(host) + return uid, key + + +@pytest.mark.parametrize( + 'model_name,get_creds', + ( + ('ERA5', get_creds_cds), + ('ERA5T', get_creds_cds), + ('HRES', get_creds_ecmwf), + ('GMAO', get_creds_netrc), + ('MERRA2', get_creds_netrc) + ) +) +def test_createFile(model_name, get_creds): + # Get the rc file's path + hidden_ext = '_' if system() == "Windows" else '.' + rc_filename = credentials.RC_FILENAMES[model_name] + if rc_filename is None: + return + rc_path = Path('./') / (hidden_ext + rc_filename) + rc_path = rc_path.expanduser() + rc_path.unlink(missing_ok=True) + + test_uid = random_string() + test_key = random_string() + + # Test creation of the rc file + credentials.check_api(model_name, test_uid, test_key, './', update_rc_file=False) + assert rc_path.exists(), f'{rc_path} does not exist' + + # Check if API is written correctly + uid, key = get_creds(rc_path) + rc_path.unlink() + assert uid == test_uid, f'{rc_path}: UID was not written correctly' + assert key == test_key, f'{rc_path}: KEY was not written correctly' \ No newline at end of file diff --git a/test/credentials/test_envVars.py b/test/credentials/test_envVars.py new file mode 100644 index 000000000..9c8bcd071 --- /dev/null +++ b/test/credentials/test_envVars.py @@ -0,0 +1,108 @@ +''' +Environment variables specific to each model are accepted iff uid and key +arguments are None. +''' + +import pytest +from pathlib import Path +from platform import system +from RAiDER.models import credentials +from test import random_string + +@pytest.mark.parametrize( + 'model_name,template,env_var_name_uid,env_var_name_key', + [ + ( + 'ERA5', ( + 'url: https://cds.climate.copernicus.eu/api/v2\n' + 'key: {uid}:{key}\n' + ), + 'RAIDER_ECMWF_ERA5_UID', + 'RAIDER_ECMWF_ERA5_API_KEY' + ), + ( + 'ERA5T', ( + 'url: https://cds.climate.copernicus.eu/api/v2\n' + 'key: {uid}:{key}\n' + ), + 'RAIDER_ECMWF_ERA5_UID', + 'RAIDER_ECMWF_ERA5_API_KEY' + ), + ( + 'HRES', ( + '{{\n' + ' "url" : "https://api.ecmwf.int/v1",\n' + ' "key" : "{key}",\n' + ' "email" : "{uid}"\n' + '}}\n' + ), + 'RAIDER_HRES_EMAIL', + 'RAIDER_HRES_API_KEY' + ), + ( + # Simulate a .netrc file with multiple sets of credentials. + # The ones not for urs.earthdata.nasa.gov should NOT be touched. + # Indentation is done with TABS, as that is what the netrc package + # generates. + 'GMAO', ( + 'machine example.com\n' + ' login johndoe\n' + ' password hunter2\n' + 'machine urs.earthdata.nasa.gov\n' + ' login {uid}\n' + ' password {key}\n' + 'machine 127.0.0.1\n' + ' login bobsmith\n' + ' password dolphins\n' + ), + 'EARTHDATA_USERNAME', + 'EARTHDATA_PASSWORD' + ), + ( + 'MERRA2', ( + 'machine example.com\n' + ' login johndoe\n' + ' password hunter2\n' + 'machine urs.earthdata.nasa.gov\n' + ' login {uid}\n' + ' password {key}\n' + 'machine 127.0.0.1\n' + ' login bobsmith\n' + ' password dolphins\n' + ), + 'EARTHDATA_USERNAME', + 'EARTHDATA_PASSWORD' + ), + ] +) +def test_envVars( + monkeypatch, + model_name, + template, + env_var_name_uid, + env_var_name_key +): + hidden_ext = '_' if system() == "Windows" else '.' + rc_filename = credentials.RC_FILENAMES[model_name] + if rc_filename is None: + return + rc_path = Path('./') / (hidden_ext + rc_filename) + rc_path = rc_path.expanduser() + rc_path.unlink(missing_ok=True) + + test_uid = random_string() + test_key = random_string() + + with monkeypatch.context() as mp: + mp.setenv(env_var_name_uid, test_uid) + mp.setenv(env_var_name_key, test_key) + credentials.check_api(model_name, None, None, './', update_rc_file=False) + + expected_content = template.format(uid=test_uid, key=test_key) + actual_content = rc_path.read_text() + rc_path.unlink() + + assert ( + expected_content == actual_content, + f'{rc_path} was not updated correctly' + ) diff --git a/test/credentials/test_updateFalse.py b/test/credentials/test_updateFalse.py new file mode 100644 index 000000000..822c58ff9 --- /dev/null +++ b/test/credentials/test_updateFalse.py @@ -0,0 +1,31 @@ +''' +When update_rc_file is False, the RC file should NOT be modified if it already +exists. +''' +import pytest + +from pathlib import Path +from platform import system +from RAiDER.models import credentials + + +@pytest.mark.parametrize('model_name', 'ERA5 ERA5T HRES GMAO MERRA2'.split()) +def test_updateFalse(model_name): + # Get the rc file's path + hidden_ext = '_' if system() == "Windows" else '.' + rc_filename = credentials.RC_FILENAMES[model_name] + if rc_filename is None: + return + rc_path = Path('./') / (hidden_ext + rc_filename) + rc_path = rc_path.expanduser() + + # Write some example text to test for + rc_path.write_text('dummy') + + # Test creation of this model's RC file in current dir + credentials.check_api(model_name, None, None, './', update_rc_file=False) + + # Assert the content was unchanged + content = rc_path.read_text() + rc_path.unlink() + assert content == 'dummy', f'{rc_path} was modified' diff --git a/test/credentials/test_updateTrue.py b/test/credentials/test_updateTrue.py new file mode 100644 index 000000000..9ed65fbc2 --- /dev/null +++ b/test/credentials/test_updateTrue.py @@ -0,0 +1,91 @@ +''' +When update_rc_file is True, the RC file should be: +- updated if it already exists, +- created if it doesn't, +- and for .netrc files, it should ONLY update the set of credentials related to + the given weather model's API URL. +''' +import pytest + +from pathlib import Path +from platform import system +from RAiDER.models import credentials +from test import random_string + + +@pytest.mark.parametrize( + 'model_name,template', + [ + ( + 'ERA5', ( + 'url: https://cds.climate.copernicus.eu/api/v2\n' + 'key: {uid}:{key}\n' + ) + ), + ( + 'ERA5T', ( + 'url: https://cds.climate.copernicus.eu/api/v2\n' + 'key: {uid}:{key}\n' + ) + ), + ( + 'HRES', ( + '{{\n' + ' "url" : "https://api.ecmwf.int/v1",\n' + ' "key" : "{key}",\n' + ' "email" : "{uid}"\n' + '}}\n' + ) + ), + ( + # Simulate a .netrc file with multiple sets of credentials. + # The ones not for urs.earthdata.nasa.gov should NOT be touched. + # Indentation is done with TABS, as that is what the netrc package + # generates. + 'GMAO', ( + 'machine example.com\n' + ' login johndoe\n' + ' password hunter2\n' + 'machine urs.earthdata.nasa.gov\n' + ' login {uid}\n' + ' password {key}\n' + 'machine 127.0.0.1\n' + ' login bobsmith\n' + ' password dolphins\n' + ) + ), + ( + 'MERRA2', ( + 'machine example.com\n' + ' login johndoe\n' + ' password hunter2\n' + 'machine urs.earthdata.nasa.gov\n' + ' login {uid}\n' + ' password {key}\n' + 'machine 127.0.0.1\n' + ' login bobsmith\n' + ' password dolphins\n' + ) + ), + ] +) +def test_updateTrue(model_name, template): + # Get the rc file's path + hidden_ext = '_' if system() == "Windows" else '.' + rc_filename = credentials.RC_FILENAMES[model_name] + if rc_filename is None: + return + rc_path = Path('./') / (hidden_ext + rc_filename) + + test_uid = random_string() + test_key = random_string() + credentials.check_api(model_name, test_uid, test_key, './', update_rc_file=True) + + expected_content = template.format(uid=test_uid, key=test_key) + actual_content = rc_path.read_text() + rc_path.unlink() + + assert ( + expected_content == actual_content, + f'{rc_path} was not updated correctly' + ) diff --git a/test/test_credentials.py b/test/test_credentials.py deleted file mode 100644 index caf0c7a60..000000000 --- a/test/test_credentials.py +++ /dev/null @@ -1,79 +0,0 @@ -import os -from platform import system -from RAiDER.models import credentials - -# Test checking/creating ECMWF_RC CAPI file -def test_ecmwfApi_createFile(): - import ecmwfapi - - #Check extension for hidden files - hidden_ext = '_' if system()=="Windows" else '.' - - # Test creation of ~/.ecmwfapirc file - ecmwf_file = os.path.expanduser('./') + hidden_ext + credentials.API_FILENAME['HRES'] - credentials.check_api('HRES', 'dummy', 'dummy', './', update_flag=True) - assert os.path.exists(ecmwf_file) == True,f'{ecmwf_file} does not exist' - - # Get existing ECMWF_API_RC env if exist - default_ecmwf_file = os.getenv("ECMWF_API_RC_FILE") - if default_ecmwf_file is None: - default_ecmwf_file = ecmwfapi.api.DEFAULT_RCFILE_PATH - - #Set it to current dir to avoid overwriting ~/.ecmwfapirc file - os.environ["ECMWF_API_RC_FILE"] = ecmwf_file - key, url, uid = ecmwfapi.api.get_apikey_values() - - # Return to default_ecmwf_file and remove local API file - os.environ["ECMWF_API_RC_FILE"] = default_ecmwf_file - os.remove(ecmwf_file) - - #Check if API is written correctly - assert uid == 'dummy', f'{ecmwf_file}: UID was not written correctly' - assert key == 'dummy', f'{ecmwf_file}: KEY was not written correctly' - - -# Test checking/creating Copernicus Climate Data Store -# CDS_RC CAPI file -def test_cdsApi_createFile(): - import cdsapi - - #Check extension for hidden files - hidden_ext = '_' if system()=="Windows" else '.' - - # Test creation of .cdsapirc file in current dir - cds_file = os.path.expanduser('./') + hidden_ext + credentials.API_FILENAME['ERA5'] - credentials.check_api('ERA5', 'dummy', 'dummy', './', update_flag=True) - assert os.path.exists(cds_file) == True,f'{cds_file} does not exist' - - # Check the content - cds_credentials = cdsapi.api.read_config(cds_file) - uid, key = cds_credentials['key'].split(':') - - # Remove local API file - os.remove(cds_file) - - assert uid == 'dummy', f'{cds_file}: UID was not written correctly' - assert key == 'dummy', f'{cds_file}: KEY was not written correctly' - -# Test checking/creating EARTHDATA_RC API file -def test_netrcApi_createFile(): - import netrc - - #Check extension for hidden files - hidden_ext = '_' if system()=="Windows" else '.' - - # Test creation of ~/.cdsapirc file - netrc_file = os.path.expanduser('./') + hidden_ext + credentials.API_FILENAME['GMAO'] - credentials.check_api('GMAO', 'dummy', 'dummy', './', update_flag=True) - assert os.path.exists(netrc_file) == True,f'{netrc_file} does not exist' - - # Check the content - host = 'urs.earthdata.nasa.gov' - netrc_credentials = netrc.netrc(netrc_file) - uid, _, key = netrc_credentials.authenticators(host) - - # Remove local API file - os.remove(netrc_file) - - assert uid == 'dummy', f'{netrc_file}: UID was not written correctly' - assert key == 'dummy', f'{netrc_file}: KEY was not written correctly' diff --git a/tools/RAiDER/models/credentials.py b/tools/RAiDER/models/credentials.py index 85f8eb2d7..70900bcaa 100644 --- a/tools/RAiDER/models/credentials.py +++ b/tools/RAiDER/models/credentials.py @@ -7,145 +7,156 @@ cdsapirc ERA5, ERA5T uid key https://cds.climate.copernicus.eu/api/v2 ecmwfapirc HRES email key https://api.ecmwf.int/v1 netrc GMAO, MERRA2 username password urs.earthdata.nasa.gov - HRRR [public access] + HRRR [public access] ''' import os from pathlib import Path from platform import system - -# Filename for the hidden file per model -API_FILENAME = {'ERA5' : 'cdsapirc', - 'ERA5T' : 'cdsapirc', - 'HRES' : 'ecmwfapirc', - 'GMAO' : 'netrc', - 'HRRR' : None - } - -# API urls -API_URLS = {'cdsapirc' : 'https://cds.climate.copernicus.eu/api/v2', - 'ecmwfapirc' : 'https://api.ecmwf.int/v1', - 'netrc' : 'urs.earthdata.nasa.gov'} - -# api credentials dict -API_CREDENTIALS_DICT = { - 'cdsapirc' : {'api' : """\ - \nurl: {host}\ - \nkey: {uid}:{key} - """, - 'help_url' : 'https://cds.climate.copernicus.eu/api-how-to' - }, - 'ecmwfapirc' : {'api' : """{{\ - \n"url" : "{host}",\ - \n"key" : "{key}",\ - \n"email" : "{uid}"\ - \n}} - """, - 'help_url' : 'https://confluence.ecmwf.int/display/WEBAPI/Access+ECMWF+Public+Datasets#AccessECMWFPublicDatasets-key' - }, - 'netrc' : {'api' : """\ - \nmachine {host}\ - \n login {uid}\ - \n password {key}\ - """, - 'help_url': 'https://wiki.earthdata.nasa.gov/display/EL/How+To+Access+Data+With+cURL+And+Wget' - } - } - -# system environmental variables for API credentials -''' -ENV variables in cdsapi and ecmwapir - -cdsapi ['cdsapirc'] : CDSAPI_KEY [UID:KEY], 'CDSAPI_URL' -ecmwfapir [ecmwfapirc] : 'ECMWF_API_KEY', 'ECMWF_API_EMAIL','ECMWF_API_URL' - -''' - -# Check if API enviroments exists -def _check_envs(model): +from typing import Dict, Optional, Tuple + +from RAiDER.logger import logger + + +# Filename for the rc file for each model +RC_FILENAMES: Dict[str, Optional[str]] = { + 'ERA5': 'cdsapirc', + 'ERA5T': 'cdsapirc', + 'HRES': 'ecmwfapirc', + 'GMAO': 'netrc', + 'MERRA2': 'netrc', + 'HRRR': None +} + +APIS = { + 'cdsapirc': { + 'template': ( + 'url: {host}\n' + 'key: {uid}:{key}\n' + ), + 'help_url': 'https://cds.climate.copernicus.eu/api-how-to', + 'default_host': 'https://cds.climate.copernicus.eu/api/v2' + }, + 'ecmwfapirc': { + 'template': ( + '{{\n' + ' "url" : "{host}",\n' + ' "key" : "{key}",\n' + ' "email" : "{uid}"\n' + '}}\n' + ), + 'help_url': 'https://confluence.ecmwf.int/display/WEBAPI/Access+ECMWF+Public+Datasets#AccessECMWFPublicDatasets-key', + 'default_host': 'https://api.ecmwf.int/v1' + }, + 'netrc': { + 'template': ( + 'machine {host}\n' + ' login {uid}\n' + ' password {key}\n' + ), + 'help_url': 'https://wiki.earthdata.nasa.gov/display/EL/How+To+Access+Data+With+cURL+And+Wget', + 'default_host': 'urs.earthdata.nasa.gov' + } +} + + +# Get the environment variables for a given weather model API +def _get_envs(model: str) -> Tuple[Optional[str], Optional[str], Optional[str]]: if model in ('ERA5', 'ERA5T'): uid = os.getenv('RAIDER_ECMWF_ERA5_UID') key = os.getenv('RAIDER_ECMWF_ERA5_API_KEY') - host = API_URLS['cdsapirc'] - - elif model in ('HRES',): + host = APIS['cdsapirc']['default_host'] + elif model == 'HRES': uid = os.getenv('RAIDER_HRES_EMAIL') key = os.getenv('RAIDER_HRES_API_KEY') - host = os.getenv('RAIDER_HRES_URL') - if host is None: - host = API_URLS['ecmwfapirc'] - - elif model in ('GMAO',): - uid = os.getenv('EARTHDATA_USERNAME') # same as in DockerizedTopsApp + host = os.getenv('RAIDER_HRES_URL', + APIS['ecmwfapirc']['default_host']) + elif model in ('GMAO', 'MERRA2'): + # same as in DockerizedTopsApp + uid = os.getenv('EARTHDATA_USERNAME') key = os.getenv('EARTHDATA_PASSWORD') - host = API_URLS['netrc'] - - else: # for HRRR + host = APIS['netrc']['default_host'] + else: # for HRRR uid, key, host = None, None, None - return uid, key, host -# Check and write MODEL API_RC_FILE for downloading weather model data + def check_api(model: str, - UID: str = None, - KEY: str = None, - output_dir : str = '~/', - update_flag: bool = False) -> None: - - # Weather model API filename - # typically stored in home dir as hidden file - api_filename = API_FILENAME[model] - - # Get API credential from os.env if UID/KEY are not inserted - if UID is None and KEY is None: - UID, KEY, URL = _check_envs(model) + uid: Optional[str] = None, + key: Optional[str] = None, + output_dir: str = '~/', + update_rc_file: bool = False) -> None: + # Weather model API RC filename + # Typically stored in home dir as a hidden file + rc_filename = RC_FILENAMES[model] + + # If the given API does not require an rc file, then there is nothing to do + # (e.g., HRRR) + if rc_filename is None: + return + + # Get the target rc file's path + hidden_ext = '_' if system() == "Windows" else '.' + rc_path = Path(output_dir) / (hidden_ext + rc_filename) + rc_path = rc_path.expanduser() + + # If the RC file doesn't exist, then create it. + # But if it does exist, only update it if the user requests. + if rc_path.exists() and not update_rc_file: + return + + # Get credentials from env vars if uid and key are not passed in + if uid is None and key is None: + uid, key, url = _get_envs(model) else: - URL = API_URLS[api_filename] - - # Get hidden ext for Windows - hidden_ext = '_' if system()=="Windows" else '.' - - # skip below if model is HRRR as it does not need API - if api_filename: - # Check if the credential api file exists - api_filename_path = Path(output_dir) / (hidden_ext + api_filename) - api_filename_path = api_filename_path.expanduser() - - # if update flag is on, overwrite existing file - if update_flag is True: - api_filename_path.unlink(missing_ok=True) - - # Check if API_RC file already exists - if api_filename_path.exists(): - return None - - # if it does not exist, put UID/KEY inserted, create it - elif not api_filename_path.exists() and UID and KEY: - # Create file with inputs, do it only once - print(f'Writing {api_filename_path} locally!') - api_filename_path.write_text(API_CREDENTIALS_DICT[api_filename]['api'].format(uid=UID, - key=KEY, - host=URL)) - api_filename_path.chmod(0o000600) - # Raise ERROR message + url = APIS[rc_filename]['default_host'] + + # Check for invalid inputs + if uid is None or key is None: + help_url = APIS[rc_filename]['help_url'] + if uid is None and key is not None: + raise ValueError( + f'ERROR: {model} API UID not provided in RAiDER arguments and ' + 'not present in environment variables.\n' + f'See info for this model\'s API at \033[1m{help_url}\033[0m' + ) + elif uid is not None and key is None: + raise ValueError( + f'ERROR: {model} API key not provided in RAiDER arguments and ' + 'not present in environment variables.\n' + f'See info for this model\'s API at \033[1m{help_url}\033[0m' + ) else: - help_url = API_CREDENTIALS_DICT[api_filename]['help_url'] - - # Raise ERROR in case only UID or KEY is inserted - if UID is not None and KEY is None: - raise ValueError(f'ERROR: API UID not inserted' - f' or does not exist in ENVIRONMENTALS!') - elif UID is None and KEY is not None: - raise ValueError(f'ERROR: API KEY not inserted' - f' or does not exist in ENVIRONMENTALS!') - else: - #Raise ERROR is both UID/KEY are none - raise ValueError( - f'{api_filename_path}, API ENVIRONMENTALS' - f' and API UID and KEY, do not exist !!' - f'\nGet API info from ' + '\033[1m' f'{help_url}' + '\033[0m, and add it!') + raise ValueError( + f'ERROR: {model} API credentials not provided in RAiDER ' + 'arguments and not present in environment variables.\n' + f'See info for this model\'s API at \033[1m{help_url}\033[0m' + ) + + # Create file with the API credentials + if update_rc_file: + logger.info(f'Updating {model} API credentials in {rc_path}') + else: + logger.warning(f'{model} API credentials not found in {rc_path}; creating') + rc_type = RC_FILENAMES[model] + if rc_type in ('cdsapirc', 'ecmwfapirc'): + # These RC files only ever contain one set of credentials, so + # they can just be overwritten when updating. + template = APIS[rc_filename]['template'] + entry = template.format(host=url, uid=uid, key=key) + rc_path.write_text(entry) + elif rc_type == 'netrc': + # This type of RC file may contain more than one set of credentials, + # so extra care needs to be taken to make sure we only touch the + # one that belongs to this URL. + import netrc + rc_path.touch() + netrc_credentials = netrc.netrc(rc_path) + netrc_credentials.hosts[url] = (uid, None, key) + rc_path.write_text(str(netrc_credentials)) + rc_path.chmod(0o000600) def setup_from_env(): - for model in API_FILENAME.keys(): + for model in RC_FILENAMES.keys(): check_api(model)