diff --git a/README.md b/README.md index 8e40833d..3140c605 100644 --- a/README.md +++ b/README.md @@ -70,22 +70,24 @@ The only requirement to use this library is to open a free account with NASA [ED ### **Authentication** -Once you have an EDL account, you can authenticate using one of the following three methods: +By default, `earthaccess` with automatically look for your EDL account credentials in two locations: -1. Using a `.netrc` file - * Can use *earthaccess* to read your EDL credentials (username and password) from a `.netrc` file -2. Reading your EDL credentials from environment variables - * if available you can use environment variables **EARTHDATA_USERNAME** and **EARTHDATA_PASSWORD** -3. Interactively entering your EDL credentials - * You can be prompted for these credentials and save them to a `.netrc` file +1. A `~/.netrc` file +2. `EARTHDATA_USERNAME` and `EARTHDATA_PASSWORD` environment variables + +If neither of these options are configured, you can authenticate by calling the `earthaccess.login()` method +and manually entering your EDL account credentials. ```python import earthaccess -auth = earthaccess.login() - +earthaccess.login() ``` +Note you can pass `persist=True` to `earthaccess.login()` to have the EDL account credentials you enter +automatically saved to a `~/.netrc` file for future use. + + Once you are authenticated with NASA EDL you can: * Get a file from a DAAC using a `fsspec` session. diff --git a/earthaccess/__init__.py b/earthaccess/__init__.py index ef4fdfb8..221b8cd1 100644 --- a/earthaccess/__init__.py +++ b/earthaccess/__init__.py @@ -1,3 +1,5 @@ +import logging +import threading from importlib.metadata import version from typing import Any @@ -18,6 +20,8 @@ from .search import DataCollections, DataGranules from .store import Store +logger = logging.getLogger(__name__) + __all__ = [ "login", "search_datasets", @@ -36,7 +40,42 @@ "Store", ] -__auth__ = Auth() -__store__: Any = None - __version__ = version("earthaccess") + +_auth = Auth() +_store = None +_lock = threading.Lock() + + +def __getattr__(name): # type: ignore + """ + Module-level getattr to handle automatic authentication when accessing + `earthaccess.__auth__` and `earthaccess.__store__`. + + Other unhandled attributes raise as `AttributeError` as expected. + """ + global _auth, _store + + if name == "__auth__" or name == "__store__": + with _lock: + if not _auth.authenticated: + for strategy in ["environment", "netrc"]: + try: + _auth.login(strategy=strategy) + except Exception as e: + logger.debug( + f"An error occurred during automatic authentication with {strategy=}: {str(e)}" + ) + continue + else: + if not _auth.authenticated: + continue + else: + _store = Store(_auth) + logger.debug( + f"Automatic authentication with {strategy=} was successful" + ) + break + return _auth if name == "__auth__" else _store + else: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/earthaccess/auth.py b/earthaccess/auth.py index 80fbcec1..9a3b22cb 100644 --- a/earthaccess/auth.py +++ b/earthaccess/auth.py @@ -1,4 +1,5 @@ import getpass +import logging import os from netrc import NetrcParseError from pathlib import Path @@ -10,6 +11,8 @@ from .daac import DAACS +logger = logging.getLogger(__name__) + class SessionWithHeaderRedirection(requests.Session): """ @@ -76,7 +79,7 @@ def login(self, strategy: str = "netrc", persist: bool = False) -> Any: an instance of Auth. """ if self.authenticated: - print("We are already authenticated with NASA EDL") + logger.debug("We are already authenticated with NASA EDL") return self if strategy == "interactive": self._interactive(persist) @@ -98,7 +101,7 @@ def refresh_tokens(self) -> bool: if resp_tokens.ok: self.token = resp_tokens.json() self.tokens = [self.token] - print( + logger.debug( f"earthaccess generated a token for CMR with expiration on: {self.token['expiration_date']}" ) return True @@ -111,7 +114,7 @@ def refresh_tokens(self) -> bool: if resp_tokens.ok: self.token = resp_tokens.json() self.tokens.extend(self.token) - print( + logger.debug( f"earthaccess generated a token for CMR with expiration on: {self.token['expiration_date']}" ) return True @@ -127,7 +130,7 @@ def refresh_tokens(self) -> bool: if resp_tokens.ok: self.token = resp_tokens.json() self.tokens[0] = self.token - print( + logger.debug( f"earthaccess generated a token for CMR with expiration on: {self.token['expiration_date']}" ) return True @@ -230,7 +233,7 @@ def _interactive(self, presist_credentials: bool = False) -> bool: password = getpass.getpass(prompt="Enter your Earthdata password: ") authenticated = self._get_credentials(username, password) if authenticated: - print("Using user provided credentials for EDL") + logger.debug("Using user provided credentials for EDL") if presist_credentials: print("Persisting credentials to .netrc") self._persist_user_credentials(username, password) @@ -240,11 +243,11 @@ def _netrc(self) -> bool: try: my_netrc = Netrc() except FileNotFoundError as err: - print(f"No .netrc found in {os.path.expanduser('~')}") - raise err + raise FileNotFoundError( + f"No .netrc found in {os.path.expanduser('~')}" + ) from err except NetrcParseError as err: - print("Unable to parse .netrc") - raise err + raise NetrcParseError("Unable to parse .netrc") from err if my_netrc["urs.earthdata.nasa.gov"] is not None: username = my_netrc["urs.earthdata.nasa.gov"]["login"] password = my_netrc["urs.earthdata.nasa.gov"]["password"] @@ -252,7 +255,7 @@ def _netrc(self) -> bool: return False authenticated = self._get_credentials(username, password) if authenticated: - print("Using .netrc file for EDL") + logger.debug("Using .netrc file for EDL") return authenticated def _environment(self) -> bool: @@ -260,9 +263,9 @@ def _environment(self) -> bool: password = os.getenv("EARTHDATA_PASSWORD") authenticated = self._get_credentials(username, password) if authenticated: - print("Using environment variables for EDL") + logger.debug("Using environment variables for EDL") else: - print( + logger.debug( "EARTHDATA_USERNAME and EARTHDATA_PASSWORD are not set in the current environment, try " "setting them or use a different strategy (netrc, interactive)" ) @@ -279,7 +282,7 @@ def _get_credentials( f"Authentication with Earthdata Login failed with:\n{token_resp.text}" ) return False - print("You're now authenticated with NASA Earthdata Login") + logger.debug("You're now authenticated with NASA Earthdata Login") self.username = username self.password = password @@ -288,13 +291,13 @@ def _get_credentials( if len(self.tokens) == 0: self.refresh_tokens() - print( + logger.debug( f"earthaccess generated a token for CMR with expiration on: {self.token['expiration_date']}" ) self.token = self.tokens[0] elif len(self.tokens) > 0: self.token = self.tokens[0] - print( + logger.debug( f"Using token with expiration date: {self.token['expiration_date']}" ) profile = self.get_user_profile()