Skip to content

Authentication API#

The shared auth contract. A provider that needs credentials exposes a <Provider>Auth / <Provider>Credentials pair in its own auth.py with environment-variable fallbacks, and raises AuthenticationError on failure.

from earthlens.base import AbstractAuth, AuthenticationError

For which provider needs what, see Supported providers; for a worked example of supplying credentials, see Authentication examples.

AbstractAuth#

earthlens.base.AbstractAuth #

Bases: ABC, Generic[CredentialsT]

Blueprint for every backend's auth class.

Concrete subclasses bind a credentials type (the CredentialsT type parameter) and implement two methods:

  • configure — perform whatever one-time setup the backend needs (call ee.Initialize, write ~/.cdsapirc, fetch an OAuth bearer, etc.). Must be idempotent: calling it after is_authenticated returns True is a no-op.
  • is_authenticated — return True when the in-process state has working credentials and the next download call will succeed without re-authenticating.

The class is a context manager: with FooAuth(creds) as auth: enters by calling configure and exits by calling close. The default close does nothing because most backends configure their SDK globally; subclasses that genuinely hold a closeable resource (e.g. an HTTP session, a boto3 client) override close to release it.

Attributes:

Name Type Description
_creds

The credentials value object passed at construction. Stored verbatim so subclasses can read individual fields (self._creds.username, self._creds.api_key). Not re-exported as a public attribute — concrete classes decide which fields are safe to surface (a service-account email is fine; a secret is not).

Examples:

  • A minimal concrete subclass that flips an internal flag:
    >>> from pydantic import BaseModel, SecretStr
    >>> from earthlens.base import AbstractAuth
    >>> class _Creds(BaseModel):
    ...     token: SecretStr
    >>> class _Auth(AbstractAuth[_Creds]):
    ...     def __init__(self, creds):
    ...         super().__init__(creds)
    ...         self._authed = False
    ...     def configure(self):
    ...         if self.is_authenticated():
    ...             return
    ...         self._authed = True
    ...     def is_authenticated(self):
    ...         return self._authed
    >>> auth = _Auth(_Creds(token="abc"))
    >>> auth.is_authenticated()
    False
    >>> auth.configure()
    >>> auth.is_authenticated()
    True
    
  • The context-manager form configures on enter:
    >>> from pydantic import BaseModel, SecretStr
    >>> from earthlens.base import AbstractAuth
    >>> class _Creds(BaseModel):
    ...     token: SecretStr
    >>> class _Auth(AbstractAuth[_Creds]):
    ...     def __init__(self, creds):
    ...         super().__init__(creds)
    ...         self._authed = False
    ...     def configure(self):
    ...         self._authed = True
    ...     def is_authenticated(self):
    ...         return self._authed
    >>> with _Auth(_Creds(token="x")) as auth:
    ...     auth.is_authenticated()
    True
    
Source code in libs/core/src/earthlens/base/auth.py
class AbstractAuth(ABC, Generic[CredentialsT]):
    """Blueprint for every backend's auth class.

    Concrete subclasses bind a credentials type (the `CredentialsT`
    type parameter) and implement two methods:

    * `configure` — perform whatever one-time setup the backend
      needs (call `ee.Initialize`, write `~/.cdsapirc`, fetch an
      OAuth bearer, etc.). Must be idempotent: calling it after
      `is_authenticated` returns `True` is a no-op.
    * `is_authenticated` — return `True` when the in-process state
      has working credentials and the next download call will
      succeed without re-authenticating.

    The class is a context manager: `with FooAuth(creds) as auth:`
    enters by calling `configure` and exits by calling `close`. The
    default `close` does nothing because most backends configure
    their SDK globally; subclasses that genuinely hold a closeable
    resource (e.g. an HTTP session, a boto3 client) override
    `close` to release it.

    Attributes:
        _creds: The credentials value object passed at
            construction. Stored verbatim so subclasses can read
            individual fields (`self._creds.username`,
            `self._creds.api_key`). Not re-exported as a public
            attribute — concrete classes decide which fields are
            safe to surface (a service-account email is fine; a
            secret is not).

    Examples:
        - A minimal concrete subclass that flips an internal flag:
            ```python
            >>> from pydantic import BaseModel, SecretStr
            >>> from earthlens.base import AbstractAuth
            >>> class _Creds(BaseModel):
            ...     token: SecretStr
            >>> class _Auth(AbstractAuth[_Creds]):
            ...     def __init__(self, creds):
            ...         super().__init__(creds)
            ...         self._authed = False
            ...     def configure(self):
            ...         if self.is_authenticated():
            ...             return
            ...         self._authed = True
            ...     def is_authenticated(self):
            ...         return self._authed
            >>> auth = _Auth(_Creds(token="abc"))
            >>> auth.is_authenticated()
            False
            >>> auth.configure()
            >>> auth.is_authenticated()
            True

            ```
        - The context-manager form configures on enter:
            ```python
            >>> from pydantic import BaseModel, SecretStr
            >>> from earthlens.base import AbstractAuth
            >>> class _Creds(BaseModel):
            ...     token: SecretStr
            >>> class _Auth(AbstractAuth[_Creds]):
            ...     def __init__(self, creds):
            ...         super().__init__(creds)
            ...         self._authed = False
            ...     def configure(self):
            ...         self._authed = True
            ...     def is_authenticated(self):
            ...         return self._authed
            >>> with _Auth(_Creds(token="x")) as auth:
            ...     auth.is_authenticated()
            True

            ```
    """

    #: Set to `True` by :meth:`mark_configured`; read by the default
    #: :meth:`is_authenticated`. Class-level so an instance that never
    #: configured still answers `False` without an `__init__` of its own.
    _configured: bool = False

    def __init__(self, credentials: CredentialsT) -> None:
        """Store the credentials value object.

        Args:
            credentials: A frozen / validated value object holding the
                secrets the backend needs (service-account email + key
                path, CDS URL + API key, EDL username + password, …).
                Type-parameterised on the concrete subclass so
                `MyAuth(MyCreds(...))` is type-checked.
        """
        self._creds = credentials

    @abstractmethod
    def configure(self) -> None:
        """Perform the one-time setup so subsequent calls work without re-auth.

        Subclasses implement this to (e.g.) call `ee.Initialize`,
        write a credentials file, or mint an OAuth bearer. Must be
        idempotent: a second call after :meth:`is_authenticated`
        returns `True` is a no-op (typical implementation is an
        early-return guarded by `if self.is_authenticated(): return`).

        Raises:
            AuthenticationError: When the credentials are
                missing/invalid or the backend rejects them.
        """

    def mark_configured(self) -> None:
        """Record that :meth:`configure` completed, for the default predicate.

        Call this at the end of a successful `configure()` so the inherited
        :meth:`is_authenticated` starts returning `True` and the next
        `configure()` short-circuits.
        """
        self._configured = True

    def is_authenticated(self) -> bool:
        """Return `True` when the in-process state has working credentials.

        Cheap predicate — must not call the network. Used by
        :meth:`configure` for idempotency and by callers that want to
        skip a redundant setup pass.

        The default reports whether :meth:`mark_configured` has run, which is
        the "did `configure()` succeed?" flag the majority of the auth classes
        each declared by hand. Override it when the real answer lives elsewhere
        — an SDK's own session object (asf), a token expiry, or a backend whose
        credentials are always present (ghsl, worldpop return `True`).
        """
        return self._configured

    def close(self) -> None:
        """Release any resource held by :meth:`configure`.

        Default: no-op. Backends whose `configure()` opens a
        long-lived HTTP session, boto3 client, or background thread
        override this to close it. Most backends configure their SDK
        globally and have nothing to release here.
        """

    def __enter__(self) -> AbstractAuth[CredentialsT]:
        self.configure()
        return self

    def __exit__(
        self,
        exc_type: type[BaseException] | None,
        exc: BaseException | None,
        tb: object | None,
    ) -> None:
        self.close()

__init__(credentials) #

Store the credentials value object.

Parameters:

Name Type Description Default
credentials CredentialsT

A frozen / validated value object holding the secrets the backend needs (service-account email + key path, CDS URL + API key, EDL username + password, …). Type-parameterised on the concrete subclass so MyAuth(MyCreds(...)) is type-checked.

required
Source code in libs/core/src/earthlens/base/auth.py
def __init__(self, credentials: CredentialsT) -> None:
    """Store the credentials value object.

    Args:
        credentials: A frozen / validated value object holding the
            secrets the backend needs (service-account email + key
            path, CDS URL + API key, EDL username + password, …).
            Type-parameterised on the concrete subclass so
            `MyAuth(MyCreds(...))` is type-checked.
    """
    self._creds = credentials

close() #

Release any resource held by :meth:configure.

Default: no-op. Backends whose configure() opens a long-lived HTTP session, boto3 client, or background thread override this to close it. Most backends configure their SDK globally and have nothing to release here.

Source code in libs/core/src/earthlens/base/auth.py
def close(self) -> None:
    """Release any resource held by :meth:`configure`.

    Default: no-op. Backends whose `configure()` opens a
    long-lived HTTP session, boto3 client, or background thread
    override this to close it. Most backends configure their SDK
    globally and have nothing to release here.
    """

configure() abstractmethod #

Perform the one-time setup so subsequent calls work without re-auth.

Subclasses implement this to (e.g.) call ee.Initialize, write a credentials file, or mint an OAuth bearer. Must be idempotent: a second call after :meth:is_authenticated returns True is a no-op (typical implementation is an early-return guarded by if self.is_authenticated(): return).

Raises:

Type Description
AuthenticationError

When the credentials are missing/invalid or the backend rejects them.

Source code in libs/core/src/earthlens/base/auth.py
@abstractmethod
def configure(self) -> None:
    """Perform the one-time setup so subsequent calls work without re-auth.

    Subclasses implement this to (e.g.) call `ee.Initialize`,
    write a credentials file, or mint an OAuth bearer. Must be
    idempotent: a second call after :meth:`is_authenticated`
    returns `True` is a no-op (typical implementation is an
    early-return guarded by `if self.is_authenticated(): return`).

    Raises:
        AuthenticationError: When the credentials are
            missing/invalid or the backend rejects them.
    """

is_authenticated() #

Return True when the in-process state has working credentials.

Cheap predicate — must not call the network. Used by :meth:configure for idempotency and by callers that want to skip a redundant setup pass.

The default reports whether :meth:mark_configured has run, which is the "did configure() succeed?" flag the majority of the auth classes each declared by hand. Override it when the real answer lives elsewhere — an SDK's own session object (asf), a token expiry, or a backend whose credentials are always present (ghsl, worldpop return True).

Source code in libs/core/src/earthlens/base/auth.py
def is_authenticated(self) -> bool:
    """Return `True` when the in-process state has working credentials.

    Cheap predicate — must not call the network. Used by
    :meth:`configure` for idempotency and by callers that want to
    skip a redundant setup pass.

    The default reports whether :meth:`mark_configured` has run, which is
    the "did `configure()` succeed?" flag the majority of the auth classes
    each declared by hand. Override it when the real answer lives elsewhere
    — an SDK's own session object (asf), a token expiry, or a backend whose
    credentials are always present (ghsl, worldpop return `True`).
    """
    return self._configured

mark_configured() #

Record that :meth:configure completed, for the default predicate.

Call this at the end of a successful configure() so the inherited :meth:is_authenticated starts returning True and the next configure() short-circuits.

Source code in libs/core/src/earthlens/base/auth.py
def mark_configured(self) -> None:
    """Record that :meth:`configure` completed, for the default predicate.

    Call this at the end of a successful `configure()` so the inherited
    :meth:`is_authenticated` starts returning `True` and the next
    `configure()` short-circuits.
    """
    self._configured = True

AuthenticationError#

earthlens.base.AuthenticationError #

Bases: Exception

Raised when a backend cannot establish an authenticated session.

Subclasses re-raise this with backend-specific context (missing ~/.cdsapirc, unregistered Earth Engine project, expired CDSE token, missing Earthdata Login). Every backend's auth class catches the underlying SDK / HTTP exception and wraps it with an actionable message — never propagates a raw cdsapi.api.Exception or ee.EEException to the user.

The class is intentionally a flat Exception subclass and not ConnectionError because half the failure modes are not network errors (no credentials at all, malformed key file, misconfigured project IAM). Callers should catch AuthenticationError directly rather than its causes.

Examples:

  • The error preserves its constructor message:
    >>> from earthlens.base import AuthenticationError
    >>> exc = AuthenticationError("missing ~/.cdsapirc")
    >>> str(exc)
    'missing ~/.cdsapirc'
    
  • Catch every backend's auth failure with one clause:
    >>> from earthlens.base import AuthenticationError
    >>> try:
    ...     raise AuthenticationError("token expired")
    ... except AuthenticationError as exc:
    ...     handled = str(exc)
    >>> handled
    'token expired'
    
Source code in libs/core/src/earthlens/base/auth.py
class AuthenticationError(Exception):
    """Raised when a backend cannot establish an authenticated session.

    Subclasses re-raise this with backend-specific context (missing
    `~/.cdsapirc`, unregistered Earth Engine project, expired CDSE
    token, missing Earthdata Login). Every backend's auth class
    catches the underlying SDK / HTTP exception and wraps it with
    an actionable message — never propagates a raw
    `cdsapi.api.Exception` or `ee.EEException` to the user.

    The class is intentionally a flat `Exception` subclass and not
    `ConnectionError` because half the failure modes are not
    network errors (no credentials at all, malformed key file,
    misconfigured project IAM). Callers should catch
    `AuthenticationError` directly rather than its causes.

    Examples:
        - The error preserves its constructor message:
            ```python
            >>> from earthlens.base import AuthenticationError
            >>> exc = AuthenticationError("missing ~/.cdsapirc")
            >>> str(exc)
            'missing ~/.cdsapirc'

            ```
        - Catch every backend's auth failure with one clause:
            ```python
            >>> from earthlens.base import AuthenticationError
            >>> try:
            ...     raise AuthenticationError("token expired")
            ... except AuthenticationError as exc:
            ...     handled = str(exc)
            >>> handled
            'token expired'

            ```
    """

S3 credentials#

Used by the two backends that resolve AWS credentials through this helper — amazon-s3 (ERA5 and the other public buckets) and dem (Copernicus DEM). The other bucket-backed backends open their clients directly.

earthlens.base.S3Auth #

Bases: AbstractAuth[S3Credentials]

Build an unsigned (or profile-signed) boto3 S3 client lazily.

Implements the :class:~earthlens.base.AbstractAuth contract for public AWS Open-Data buckets. configure() constructs the client on first use (importing boto3 lazily so the package imports without the [s3] extra); is_authenticated() reports whether the client exists; :meth:client is the accessor the backend's fetch step calls.

Because the target buckets are public, "authenticated" simply means "a client has been built" — there is no token to mint or expire.

Examples:

  • The client is not built until configure() runs:
    >>> from earthlens.base.s3 import S3Auth, S3Credentials
    >>> auth = S3Auth(S3Credentials())
    >>> auth.is_authenticated()
    False
    
Source code in libs/core/src/earthlens/base/s3.py
class S3Auth(AbstractAuth[S3Credentials]):
    """Build an unsigned (or profile-signed) `boto3` S3 client lazily.

    Implements the :class:`~earthlens.base.AbstractAuth` contract for
    public AWS Open-Data buckets. `configure()` constructs the client on
    first use (importing `boto3` lazily so the package imports without
    the `[s3]` extra); `is_authenticated()` reports whether the client
    exists; :meth:`client` is the accessor the backend's fetch step
    calls.

    Because the target buckets are public, "authenticated" simply means
    "a client has been built" — there is no token to mint or expire.

    Examples:
        - The client is not built until `configure()` runs:
            ```python
            >>> from earthlens.base.s3 import S3Auth, S3Credentials
            >>> auth = S3Auth(S3Credentials())
            >>> auth.is_authenticated()
            False

            ```
    """

    def __init__(self, credentials: S3Credentials | None = None) -> None:
        """Store credentials and reset the (lazily built) client.

        Args:
            credentials: The :class:`S3Credentials` to use. `None`
                defaults to unsigned public access.
        """
        super().__init__(credentials or S3Credentials())
        self._client: Any = None

    def configure(self) -> None:
        """Build the `boto3` S3 client if it does not exist yet.

        Idempotent: returns immediately once :meth:`is_authenticated`
        is `True`. Imports `boto3` / `botocore` lazily so importing the
        package without the `[s3]` extra does not fail.

        Raises:
            ImportError: When the `[s3]` extra (`boto3`) is not
                installed. The message names `earthlens[s3]`.
        """
        if self.is_authenticated():
            return
        try:
            import boto3
            import botocore.client
        except ImportError as exc:  # pragma: no cover - exercised via monkeypatch
            raise ImportError(
                "The Amazon S3 backend requires the optional 'boto3' "
                "dependency. Install it with: pip install earthlens[s3]"
            ) from exc

        region = self._creds.region
        if self._creds.aws_profile:
            session = boto3.Session(profile_name=self._creds.aws_profile)
            self._client = session.client("s3", region_name=region)
        elif self._creds.signed:
            # Requester-pays buckets need a signed client (default credential
            # chain); the caller's AWS account is billed for the requests.
            self._client = boto3.client("s3", region_name=region)
        else:
            self._client = boto3.client(
                "s3",
                region_name=region,
                config=botocore.client.Config(signature_version=botocore.UNSIGNED),
            )

    def is_authenticated(self) -> bool:
        """Return `True` once the S3 client has been built."""
        return self._client is not None

    def client(self) -> Any:
        """Return the S3 client, building it on first access.

        Returns:
            The configured `boto3` S3 client.
        """
        self.configure()
        return self._client

    def close(self) -> None:
        """Drop the reference to the S3 client."""
        self._client = None

__init__(credentials=None) #

Store credentials and reset the (lazily built) client.

Parameters:

Name Type Description Default
credentials S3Credentials | None

The :class:S3Credentials to use. None defaults to unsigned public access.

None
Source code in libs/core/src/earthlens/base/s3.py
def __init__(self, credentials: S3Credentials | None = None) -> None:
    """Store credentials and reset the (lazily built) client.

    Args:
        credentials: The :class:`S3Credentials` to use. `None`
            defaults to unsigned public access.
    """
    super().__init__(credentials or S3Credentials())
    self._client: Any = None

client() #

Return the S3 client, building it on first access.

Returns:

Type Description
Any

The configured boto3 S3 client.

Source code in libs/core/src/earthlens/base/s3.py
def client(self) -> Any:
    """Return the S3 client, building it on first access.

    Returns:
        The configured `boto3` S3 client.
    """
    self.configure()
    return self._client

close() #

Drop the reference to the S3 client.

Source code in libs/core/src/earthlens/base/s3.py
def close(self) -> None:
    """Drop the reference to the S3 client."""
    self._client = None

configure() #

Build the boto3 S3 client if it does not exist yet.

Idempotent: returns immediately once :meth:is_authenticated is True. Imports boto3 / botocore lazily so importing the package without the [s3] extra does not fail.

Raises:

Type Description
ImportError

When the [s3] extra (boto3) is not installed. The message names earthlens[s3].

Source code in libs/core/src/earthlens/base/s3.py
def configure(self) -> None:
    """Build the `boto3` S3 client if it does not exist yet.

    Idempotent: returns immediately once :meth:`is_authenticated`
    is `True`. Imports `boto3` / `botocore` lazily so importing the
    package without the `[s3]` extra does not fail.

    Raises:
        ImportError: When the `[s3]` extra (`boto3`) is not
            installed. The message names `earthlens[s3]`.
    """
    if self.is_authenticated():
        return
    try:
        import boto3
        import botocore.client
    except ImportError as exc:  # pragma: no cover - exercised via monkeypatch
        raise ImportError(
            "The Amazon S3 backend requires the optional 'boto3' "
            "dependency. Install it with: pip install earthlens[s3]"
        ) from exc

    region = self._creds.region
    if self._creds.aws_profile:
        session = boto3.Session(profile_name=self._creds.aws_profile)
        self._client = session.client("s3", region_name=region)
    elif self._creds.signed:
        # Requester-pays buckets need a signed client (default credential
        # chain); the caller's AWS account is billed for the requests.
        self._client = boto3.client("s3", region_name=region)
    else:
        self._client = boto3.client(
            "s3",
            region_name=region,
            config=botocore.client.Config(signature_version=botocore.UNSIGNED),
        )

is_authenticated() #

Return True once the S3 client has been built.

Source code in libs/core/src/earthlens/base/s3.py
def is_authenticated(self) -> bool:
    """Return `True` once the S3 client has been built."""
    return self._client is not None

earthlens.base.S3Credentials #

Bases: BaseModel

Credentials for the AWS S3 backend.

The seeded datasets are all public, so the default (all fields unset) yields an unsigned client. Set aws_profile to sign requests with a named profile from the local AWS configuration — needed only for signed / requester-pays buckets, which are out of the first-cut scope.

Attributes:

Name Type Description
aws_profile str | None

Name of a profile in ~/.aws/credentials / ~/.aws/config to sign requests with. None (the default) builds an unsigned client suitable for public buckets.

signed bool

Force a signed client from the default credential chain (without naming a profile) — used for requester-pays buckets.

region str | None

AWS region to build the client in (None = default).

Examples:

  • The default is unsigned (no profile):
    >>> from earthlens.base.s3 import S3Credentials
    >>> S3Credentials().aws_profile is None
    True
    
Source code in libs/core/src/earthlens/base/s3.py
class S3Credentials(BaseModel):
    """Credentials for the AWS S3 backend.

    The seeded datasets are all public, so the default (all fields
    unset) yields an unsigned client. Set `aws_profile` to sign requests
    with a named profile from the local AWS configuration — needed only
    for signed / requester-pays buckets, which are out of the first-cut
    scope.

    Attributes:
        aws_profile: Name of a profile in `~/.aws/credentials` /
            `~/.aws/config` to sign requests with. `None` (the default)
            builds an unsigned client suitable for public buckets.
        signed: Force a signed client from the default credential chain
            (without naming a profile) — used for requester-pays buckets.
        region: AWS region to build the client in (`None` = default).

    Examples:
        - The default is unsigned (no profile):
            ```python
            >>> from earthlens.base.s3 import S3Credentials
            >>> S3Credentials().aws_profile is None
            True

            ```
    """

    model_config = ConfigDict(frozen=True, extra="forbid")

    aws_profile: str | None = None
    signed: bool = False
    region: str | None = None