Skip to content

JAXA — API reference#

JAXA Earth-observation archive backend — earthlens.jaxa. Background, usage, datasets, and credentials are covered under the other pages in this section; this page is the rendered API.

earthlens.jaxa #

JAXA backend — Earth-observation archive over three protocols.

earthlens.jaxa reaches JAXA's Earth-observation catalogue via three complementary SDKs, selected per-dataset by a protocol discriminator:

  • protocol: jaxa-earth — authless STAC + COG access through the official jaxa.earth API (AW3D30 elevation, GSMaP precipitation, AMSR2 L3 re-hosts, JASMES MODIS re-hosts, …). The API returns in-memory numpy arrays which the backend writes to north-up GeoTIFFs via pyramids.dataset.Dataset.create_from_array.
  • protocol: gportal — credentialed SFTP access through the community gportal SDK (raw L1/L2 swaths of GCOM-W AMSR2 / GCOM-C SGLI / ALOS-2 PALSAR-2 / EarthCARE / GPM / …). Requires a free G-Portal account; JaxaAuth(creds, protocol="gportal").configure() resolves the credentials from explicit kwargs or $GPORTAL_USERNAME / $GPORTAL_PASSWORD.
  • protocol: ptree — credentialed plain-FTP access to ftp.ptree.jaxa.jp for near-real-time Himawari-8/9 AHI HSD granules (30-day rolling archive, 10-min cadence, 10 segments per band per slot). Uses stdlib ftplib only — no additional dependency. Requires a free P-Tree account (separate from G-Portal); JaxaAuth(creds, protocol="ptree").configure() resolves the credentials from explicit kwargs or $JAXA_PTREE_USERNAME / $JAXA_PTREE_PASSWORD. Ships raw .DAT.bz2 granules only — decoding HSD to arrays is satpy's job (tracked as pyramids PY-2).

Importing this subpackage does not require the [jaxa] extra — jaxa.earth and gportal are imported inside their branch modules lazily, and the ptree branch uses only stdlib. Install pip install 'earthlens[jaxa]' to enable the two SDK-backed branches.

AuthenticationError #

Bases: AuthenticationError

Raised when a credentialed JAXA branch has no usable credentials.

Raised by the gportal and ptree branches; the jaxa-earth branch is authless. The message names the exact fix for the missing pair: for gportal, pass gportal_username= / gportal_password= to JAXA(...) or set $GPORTAL_USERNAME / $GPORTAL_PASSWORD; for ptree, pass ptree_username= / ptree_password= or set $JAXA_PTREE_USERNAME / $JAXA_PTREE_PASSWORD.

Source code in src/earthlens/jaxa/auth.py
class AuthenticationError(_BaseAuthenticationError):
    """Raised when a credentialed JAXA branch has no usable credentials.

    Raised by the `gportal` and `ptree` branches; the `jaxa-earth` branch is
    authless. The message names the exact fix for the missing pair: for
    `gportal`, pass `gportal_username=` / `gportal_password=` to `JAXA(...)`
    or set `$GPORTAL_USERNAME` / `$GPORTAL_PASSWORD`; for `ptree`, pass
    `ptree_username=` / `ptree_password=` or set `$JAXA_PTREE_USERNAME` /
    `$JAXA_PTREE_PASSWORD`.
    """

Catalog #

Bases: AbstractCatalog

Reader for the bundled JAXA dataset catalog.

Subclasses :class:AbstractCatalog so the dict-like surface (cat["aw3d30"], "aw3d30" in cat, len(cat)) comes for free. The resolve method maps a friendly alias ("elevation") to its canonical key ("aw3d30"); :meth:by_protocol lists the keys filtered by protocol; :meth:get is a thin alias for get_dataset.

Attributes:

Name Type Description
datasets dict[str, Dataset]

Map from canonical key to its :class:Dataset row.

available_datasets list[str]

Sorted canonical keys.

Examples:

  • List the canonical keys, resolve a friendly alias:
    >>> from earthlens.jaxa import Catalog
    >>> cat = Catalog()
    >>> "aw3d30" in cat
    True
    >>> cat.resolve("elevation")
    'aw3d30'
    
Source code in src/earthlens/jaxa/catalog.py
class Catalog(AbstractCatalog):
    """Reader for the bundled JAXA dataset catalog.

    Subclasses :class:`AbstractCatalog` so the dict-like surface
    (`cat["aw3d30"]`, `"aw3d30" in cat`, `len(cat)`) comes for free. The
    `resolve` method maps a friendly alias (`"elevation"`) to its canonical
    key (`"aw3d30"`); :meth:`by_protocol` lists the keys filtered by
    protocol; :meth:`get` is a thin alias for `get_dataset`.

    Attributes:
        datasets: Map from canonical key to its :class:`Dataset` row.
        available_datasets: Sorted canonical keys.

    Examples:
        - List the canonical keys, resolve a friendly alias:
            ```python
            >>> from earthlens.jaxa import Catalog
            >>> cat = Catalog()
            >>> "aw3d30" in cat
            True
            >>> cat.resolve("elevation")
            'aw3d30'

            ```
    """

    _catalog_kind: str = "JAXA catalog"

    datasets: dict[str, Dataset] = Field(default_factory=dict)
    aliases: dict[str, str] = Field(default_factory=dict)

    def model_post_init(self, __context: Any) -> None:
        """Auto-load the bundled catalog when no datasets were supplied.

        `Catalog()` with no args reads :data:`CATALOG_PATH` (cached by
        `(path, mtime)`); passing `datasets=...` skips the disk read (used
        in tests). Either way the `aliases` map is derived from the loaded
        rows. `available_datasets` is populated from the YAML's optional
        `available_datasets:` block (the refresh CLI's `--write` target —
        an informational index of every live SDK id across all three
        protocols) or, when that block is absent, defaults to the sorted
        curated keys so the dict-like surface (`len(cat)` etc.) still
        works.
        """
        if not self.datasets:
            loaded = Catalog.load()
            self.datasets = loaded.datasets
            if loaded.available_datasets and not self.available_datasets:
                self.available_datasets = loaded.available_datasets
        # Build the alias index from the rows' `aliases` lists. Conflicts
        # (same alias on two rows) raise; canonical keys also resolve to
        # themselves so `resolve()` is total over both.
        aliases: dict[str, str] = {}
        for key, ds in self.datasets.items():
            aliases[key] = key
            for alias in ds.aliases:
                if alias in aliases and aliases[alias] != key:
                    raise ValueError(
                        f"alias {alias!r} is claimed by both {aliases[alias]!r} "
                        f"and {key!r} — pick one."
                    )
                aliases[alias] = key
        self.aliases = aliases
        if not self.available_datasets:
            self.available_datasets = sorted(self.datasets)

    @classmethod
    def load(cls, catalog_path: Path | None = None) -> Catalog:
        """Read and validate the JAXA catalog from disk (cached).

        Args:
            catalog_path: Path to the catalog directory (the sharded
                default) or a single YAML file (tests / legacy). Defaults
                to the module-level :data:`CATALOG_PATH`.

        Returns:
            A fully-populated :class:`Catalog` merged across every
            contributing file.

        Raises:
            ValueError: If the path has no `datasets:` rows across any
                file, or any row fails validation (missing identifier,
                alias conflict), or a key is declared in two shards.
            FileNotFoundError: If `catalog_path` does not exist (the
                bundled catalog ships with the wheel, so this only fires
                when a caller passes an explicit, missing path).
        """
        path = catalog_path if catalog_path is not None else CATALOG_PATH
        files = _yaml_files_for(path)
        try:
            mtime_tuple = tuple((str(f), f.stat().st_mtime_ns) for f in files)
        except FileNotFoundError:
            mtime_tuple = ((str(path.resolve()), 0),)
        cache_key = (str(path.resolve()), mtime_tuple)
        cached = _CATALOG_CACHE.get(cache_key)
        if cached is not None:
            return cached

        merged_rows: dict[str, dict[str, Any]] = {}
        row_origin: dict[str, Path] = {}
        available: list[str] = []
        for file_path in files:
            data = load_yaml_strict(file_path) or {}
            for ident in data.get("available_datasets") or []:
                available.append(str(ident))
            for key, body in (data.get("datasets") or {}).items():
                if key in merged_rows:
                    raise ValueError(
                        f"dataset {key!r} declared in two catalog files: "
                        f"{row_origin[key]} and {file_path}"
                    )
                merged_rows[key] = dict(body or {})
                row_origin[key] = file_path

        if not merged_rows:
            raise ValueError(
                f"{path} is missing or has an empty 'datasets:' block. "
                "The JAXA catalog must list at least one dataset."
            )
        datasets: dict[str, Dataset] = {}
        for key, body in merged_rows.items():
            try:
                datasets[key] = Dataset(key=key, **body)
            except ValidationError as exc:
                raise ValueError(
                    f"{row_origin[key]} dataset {key!r} failed validation:\n{exc}"
                ) from exc
        catalog = cls(datasets=datasets, available_datasets=available)
        _CATALOG_CACHE[cache_key] = catalog
        return catalog

    def get_catalog(self) -> dict[str, Dataset]:
        """Return the dataset map (satisfies the abstract contract).

        Returns:
            dict[str, Dataset]: Same object as :attr:`datasets`.

        Examples:
            - Inspect one of the rows by canonical key:
                ```python
                >>> from earthlens.jaxa import Catalog
                >>> cat = Catalog()
                >>> cat.get_catalog()["aw3d30"].protocol
                'jaxa-earth'

                ```
        """
        return self.datasets

    def get(self, key: str) -> Dataset:
        """Return the :class:`Dataset` for `key` (canonical, alias, or raw id).

        Resolves `key` through :meth:`resolve` first, then returns the
        matching curated row when one exists. **Raw G-Portal numeric ids**
        (e.g. ``"11001002"``) and **raw jaxa.earth STAC collection names**
        (e.g. ``"JAXA.G-Portal_GCOM-W.AMSR2_standard.L3-SSW.daytime.v4_global_yearly"``)
        pass through unmapped: a passthrough :class:`Dataset` row is
        synthesized on the fly with the right protocol so the backend
        dispatches correctly — the full 799-entry G-Portal universe + the
        ~119-entry JAXA Earth STAC catalogue are usable without bloating
        the bundled YAML with hand-curated rows.

        Args:
            key: A canonical key, friendly alias, raw G-Portal id, or raw
                jaxa.earth STAC collection name.

        Returns:
            Dataset: The matching curated row, or a synthesized passthrough
            row when `key` is a raw id recognised by the live SDK.

        Raises:
            ValueError: If `key` is neither curated nor a syntactically
                valid raw id.

        Examples:
            - A friendly alias resolves to the same row as the canonical key:
                ```python
                >>> from earthlens.jaxa import Catalog
                >>> cat = Catalog()
                >>> cat.get("elevation").collection
                'JAXA.EORC_ALOS.PRISM_AW3D30.v3.2_global'
                >>> cat.get("aw3d30").default_band
                'DSM'

                ```
            - A raw G-Portal id passes through as a synthesized gportal row:
                ```python
                >>> from earthlens.jaxa import Catalog
                >>> row = Catalog().get("11001002")
                >>> row.protocol, row.short_name
                ('gportal', '11001002')

                ```
            - An unknown key raises ValueError:
                ```python
                >>> from earthlens.jaxa import Catalog
                >>> try:
                ...     Catalog().get("definitely-not-a-key")
                ... except ValueError as exc:
                ...     "JAXA catalog" in str(exc)
                True

                ```
        """
        canonical = self.resolve(key)
        if canonical in self.datasets:
            return self.datasets[canonical]
        # Passthrough: synthesize a Dataset row for raw ids that match
        # either protocol's id shape (resolve() has already accepted them).
        if _GPORTAL_ID_RE.match(canonical):
            return Dataset(key=canonical, protocol="gportal", short_name=canonical)
        return Dataset(key=canonical, protocol="jaxa-earth", collection=canonical)

    def resolve(self, key: str) -> str:
        """Map a friendly alias / raw id to its canonical key.

        Args:
            key: A canonical key, friendly alias, raw G-Portal numeric
                id (e.g. ``"11001002"``), or raw jaxa.earth STAC
                collection name (e.g. ``"JAXA.EORC_ALOS.PRISM_AW3D30.v3.2_global"``).

        Returns:
            str: The canonical catalog key. Raw ids pass through unchanged.

        Raises:
            ValueError: When `key` is neither curated nor a syntactically
                valid raw id, with a did-you-mean hint drawn from the
                union of canonical keys and aliases.

        Examples:
            - A canonical key resolves to itself; an alias resolves to its row:
                ```python
                >>> from earthlens.jaxa import Catalog
                >>> cat = Catalog()
                >>> cat.resolve("aw3d30")
                'aw3d30'
                >>> cat.resolve("elevation")
                'aw3d30'

                ```
            - Raw G-Portal numeric ids pass through unmapped:
                ```python
                >>> from earthlens.jaxa import Catalog
                >>> Catalog().resolve("11001002")
                '11001002'

                ```
        """
        if key in self.aliases:
            return self.aliases[key]
        # Raw G-Portal id (7-9 digits) or raw jaxa.earth STAC collection name
        # passes through unmapped, mirroring `usgs_water.Catalog.resolve` for
        # raw 5-digit NWIS codes. The available_datasets: index lets a
        # curator verify the id is real upstream.
        if _GPORTAL_ID_RE.match(key) or _JAXA_EARTH_PREFIX_RE.match(key):
            return key
        close = difflib.get_close_matches(key, self.aliases, n=1)
        hint = f" Did you mean {close[0]!r}?" if close else ""
        raise ValueError(
            f"{key!r} is not in the JAXA catalog. "
            f"Known keys: {sorted(self.datasets)}.{hint}"
        )

    def by_protocol(self, protocol: JaxaProtocol) -> list[str]:
        """Return canonical keys whose `protocol` matches `protocol`.

        Args:
            protocol: One of `"jaxa-earth"`, `"gportal"`, or `"ptree"`.

        Returns:
            list[str]: Sorted canonical keys filtered by protocol.

        Examples:
            - The bundled catalog ships all three protocols; `aw3d30` is
              jaxa-earth, `sgli-l3-nwlr` is gportal, and
              `himawari-ahi-fldk` is ptree:
                ```python
                >>> from earthlens.jaxa import Catalog
                >>> cat = Catalog()
                >>> "aw3d30" in cat.by_protocol("jaxa-earth")
                True
                >>> "sgli-l3-nwlr" in cat.by_protocol("gportal")
                True
                >>> "himawari-ahi-fldk" in cat.by_protocol("ptree")
                True

                ```
        """
        return sorted(k for k, ds in self.datasets.items() if ds.protocol == protocol)

    def __contains__(self, name: object) -> bool:
        """`name in cat` — accept both canonical keys and friendly aliases.

        Overrides :meth:`AbstractCatalog.__contains__`, which only checks
        `self.datasets`, so the alias surface (`"elevation" in cat`)
        matches what `cat.get("elevation")` already accepts.

        Examples:
            - Aliases and canonical keys both resolve:
                ```python
                >>> from earthlens.jaxa import Catalog
                >>> cat = Catalog()
                >>> "aw3d30" in cat
                True
                >>> "elevation" in cat
                True
                >>> "not-a-real-key" in cat
                False

                ```
        """
        return name in self.aliases

    def __getitem__(self, name: str) -> Dataset:
        """`cat[name]` — accept both canonical keys and friendly aliases.

        Overrides :meth:`AbstractCatalog.__getitem__` so dict-style
        lookup matches the same alias surface as :meth:`get`. Raises
        :class:`KeyError` on miss (the dict-style contract; callers that
        want a did-you-mean hint use :meth:`get` instead).

        Examples:
            - Lookup by alias returns the canonical row:
                ```python
                >>> from earthlens.jaxa import Catalog
                >>> cat = Catalog()
                >>> cat["elevation"].key
                'aw3d30'

                ```
        """
        try:
            return self.get(name)
        except ValueError as exc:
            raise KeyError(name) from exc

__contains__(name) #

name in cat — accept both canonical keys and friendly aliases.

Overrides :meth:AbstractCatalog.__contains__, which only checks self.datasets, so the alias surface ("elevation" in cat) matches what cat.get("elevation") already accepts.

Examples:

  • Aliases and canonical keys both resolve:
    >>> from earthlens.jaxa import Catalog
    >>> cat = Catalog()
    >>> "aw3d30" in cat
    True
    >>> "elevation" in cat
    True
    >>> "not-a-real-key" in cat
    False
    
Source code in src/earthlens/jaxa/catalog.py
def __contains__(self, name: object) -> bool:
    """`name in cat` — accept both canonical keys and friendly aliases.

    Overrides :meth:`AbstractCatalog.__contains__`, which only checks
    `self.datasets`, so the alias surface (`"elevation" in cat`)
    matches what `cat.get("elevation")` already accepts.

    Examples:
        - Aliases and canonical keys both resolve:
            ```python
            >>> from earthlens.jaxa import Catalog
            >>> cat = Catalog()
            >>> "aw3d30" in cat
            True
            >>> "elevation" in cat
            True
            >>> "not-a-real-key" in cat
            False

            ```
    """
    return name in self.aliases

__getitem__(name) #

cat[name] — accept both canonical keys and friendly aliases.

Overrides :meth:AbstractCatalog.__getitem__ so dict-style lookup matches the same alias surface as :meth:get. Raises :class:KeyError on miss (the dict-style contract; callers that want a did-you-mean hint use :meth:get instead).

Examples:

  • Lookup by alias returns the canonical row:
    >>> from earthlens.jaxa import Catalog
    >>> cat = Catalog()
    >>> cat["elevation"].key
    'aw3d30'
    
Source code in src/earthlens/jaxa/catalog.py
def __getitem__(self, name: str) -> Dataset:
    """`cat[name]` — accept both canonical keys and friendly aliases.

    Overrides :meth:`AbstractCatalog.__getitem__` so dict-style
    lookup matches the same alias surface as :meth:`get`. Raises
    :class:`KeyError` on miss (the dict-style contract; callers that
    want a did-you-mean hint use :meth:`get` instead).

    Examples:
        - Lookup by alias returns the canonical row:
            ```python
            >>> from earthlens.jaxa import Catalog
            >>> cat = Catalog()
            >>> cat["elevation"].key
            'aw3d30'

            ```
    """
    try:
        return self.get(name)
    except ValueError as exc:
        raise KeyError(name) from exc

by_protocol(protocol) #

Return canonical keys whose protocol matches protocol.

Parameters:

Name Type Description Default
protocol JaxaProtocol

One of "jaxa-earth", "gportal", or "ptree".

required

Returns:

Type Description
list[str]

list[str]: Sorted canonical keys filtered by protocol.

Examples:

  • The bundled catalog ships all three protocols; aw3d30 is jaxa-earth, sgli-l3-nwlr is gportal, and himawari-ahi-fldk is ptree:
    >>> from earthlens.jaxa import Catalog
    >>> cat = Catalog()
    >>> "aw3d30" in cat.by_protocol("jaxa-earth")
    True
    >>> "sgli-l3-nwlr" in cat.by_protocol("gportal")
    True
    >>> "himawari-ahi-fldk" in cat.by_protocol("ptree")
    True
    
Source code in src/earthlens/jaxa/catalog.py
def by_protocol(self, protocol: JaxaProtocol) -> list[str]:
    """Return canonical keys whose `protocol` matches `protocol`.

    Args:
        protocol: One of `"jaxa-earth"`, `"gportal"`, or `"ptree"`.

    Returns:
        list[str]: Sorted canonical keys filtered by protocol.

    Examples:
        - The bundled catalog ships all three protocols; `aw3d30` is
          jaxa-earth, `sgli-l3-nwlr` is gportal, and
          `himawari-ahi-fldk` is ptree:
            ```python
            >>> from earthlens.jaxa import Catalog
            >>> cat = Catalog()
            >>> "aw3d30" in cat.by_protocol("jaxa-earth")
            True
            >>> "sgli-l3-nwlr" in cat.by_protocol("gportal")
            True
            >>> "himawari-ahi-fldk" in cat.by_protocol("ptree")
            True

            ```
    """
    return sorted(k for k, ds in self.datasets.items() if ds.protocol == protocol)

get(key) #

Return the :class:Dataset for key (canonical, alias, or raw id).

Resolves key through :meth:resolve first, then returns the matching curated row when one exists. Raw G-Portal numeric ids (e.g. "11001002") and raw jaxa.earth STAC collection names (e.g. "JAXA.G-Portal_GCOM-W.AMSR2_standard.L3-SSW.daytime.v4_global_yearly") pass through unmapped: a passthrough :class:Dataset row is synthesized on the fly with the right protocol so the backend dispatches correctly — the full 799-entry G-Portal universe + the ~119-entry JAXA Earth STAC catalogue are usable without bloating the bundled YAML with hand-curated rows.

Parameters:

Name Type Description Default
key str

A canonical key, friendly alias, raw G-Portal id, or raw jaxa.earth STAC collection name.

required

Returns:

Name Type Description
Dataset Dataset

The matching curated row, or a synthesized passthrough

Dataset

row when key is a raw id recognised by the live SDK.

Raises:

Type Description
ValueError

If key is neither curated nor a syntactically valid raw id.

Examples:

  • A friendly alias resolves to the same row as the canonical key:
    >>> from earthlens.jaxa import Catalog
    >>> cat = Catalog()
    >>> cat.get("elevation").collection
    'JAXA.EORC_ALOS.PRISM_AW3D30.v3.2_global'
    >>> cat.get("aw3d30").default_band
    'DSM'
    
  • A raw G-Portal id passes through as a synthesized gportal row:
    >>> from earthlens.jaxa import Catalog
    >>> row = Catalog().get("11001002")
    >>> row.protocol, row.short_name
    ('gportal', '11001002')
    
  • An unknown key raises ValueError:
    >>> from earthlens.jaxa import Catalog
    >>> try:
    ...     Catalog().get("definitely-not-a-key")
    ... except ValueError as exc:
    ...     "JAXA catalog" in str(exc)
    True
    
Source code in src/earthlens/jaxa/catalog.py
def get(self, key: str) -> Dataset:
    """Return the :class:`Dataset` for `key` (canonical, alias, or raw id).

    Resolves `key` through :meth:`resolve` first, then returns the
    matching curated row when one exists. **Raw G-Portal numeric ids**
    (e.g. ``"11001002"``) and **raw jaxa.earth STAC collection names**
    (e.g. ``"JAXA.G-Portal_GCOM-W.AMSR2_standard.L3-SSW.daytime.v4_global_yearly"``)
    pass through unmapped: a passthrough :class:`Dataset` row is
    synthesized on the fly with the right protocol so the backend
    dispatches correctly — the full 799-entry G-Portal universe + the
    ~119-entry JAXA Earth STAC catalogue are usable without bloating
    the bundled YAML with hand-curated rows.

    Args:
        key: A canonical key, friendly alias, raw G-Portal id, or raw
            jaxa.earth STAC collection name.

    Returns:
        Dataset: The matching curated row, or a synthesized passthrough
        row when `key` is a raw id recognised by the live SDK.

    Raises:
        ValueError: If `key` is neither curated nor a syntactically
            valid raw id.

    Examples:
        - A friendly alias resolves to the same row as the canonical key:
            ```python
            >>> from earthlens.jaxa import Catalog
            >>> cat = Catalog()
            >>> cat.get("elevation").collection
            'JAXA.EORC_ALOS.PRISM_AW3D30.v3.2_global'
            >>> cat.get("aw3d30").default_band
            'DSM'

            ```
        - A raw G-Portal id passes through as a synthesized gportal row:
            ```python
            >>> from earthlens.jaxa import Catalog
            >>> row = Catalog().get("11001002")
            >>> row.protocol, row.short_name
            ('gportal', '11001002')

            ```
        - An unknown key raises ValueError:
            ```python
            >>> from earthlens.jaxa import Catalog
            >>> try:
            ...     Catalog().get("definitely-not-a-key")
            ... except ValueError as exc:
            ...     "JAXA catalog" in str(exc)
            True

            ```
    """
    canonical = self.resolve(key)
    if canonical in self.datasets:
        return self.datasets[canonical]
    # Passthrough: synthesize a Dataset row for raw ids that match
    # either protocol's id shape (resolve() has already accepted them).
    if _GPORTAL_ID_RE.match(canonical):
        return Dataset(key=canonical, protocol="gportal", short_name=canonical)
    return Dataset(key=canonical, protocol="jaxa-earth", collection=canonical)

get_catalog() #

Return the dataset map (satisfies the abstract contract).

Returns:

Type Description
dict[str, Dataset]

dict[str, Dataset]: Same object as :attr:datasets.

Examples:

  • Inspect one of the rows by canonical key:
    >>> from earthlens.jaxa import Catalog
    >>> cat = Catalog()
    >>> cat.get_catalog()["aw3d30"].protocol
    'jaxa-earth'
    
Source code in src/earthlens/jaxa/catalog.py
def get_catalog(self) -> dict[str, Dataset]:
    """Return the dataset map (satisfies the abstract contract).

    Returns:
        dict[str, Dataset]: Same object as :attr:`datasets`.

    Examples:
        - Inspect one of the rows by canonical key:
            ```python
            >>> from earthlens.jaxa import Catalog
            >>> cat = Catalog()
            >>> cat.get_catalog()["aw3d30"].protocol
            'jaxa-earth'

            ```
    """
    return self.datasets

load(catalog_path=None) classmethod #

Read and validate the JAXA catalog from disk (cached).

Parameters:

Name Type Description Default
catalog_path Path | None

Path to the catalog directory (the sharded default) or a single YAML file (tests / legacy). Defaults to the module-level :data:CATALOG_PATH.

None

Returns:

Type Description
Catalog

A fully-populated :class:Catalog merged across every

Catalog

contributing file.

Raises:

Type Description
ValueError

If the path has no datasets: rows across any file, or any row fails validation (missing identifier, alias conflict), or a key is declared in two shards.

FileNotFoundError

If catalog_path does not exist (the bundled catalog ships with the wheel, so this only fires when a caller passes an explicit, missing path).

Source code in src/earthlens/jaxa/catalog.py
@classmethod
def load(cls, catalog_path: Path | None = None) -> Catalog:
    """Read and validate the JAXA catalog from disk (cached).

    Args:
        catalog_path: Path to the catalog directory (the sharded
            default) or a single YAML file (tests / legacy). Defaults
            to the module-level :data:`CATALOG_PATH`.

    Returns:
        A fully-populated :class:`Catalog` merged across every
        contributing file.

    Raises:
        ValueError: If the path has no `datasets:` rows across any
            file, or any row fails validation (missing identifier,
            alias conflict), or a key is declared in two shards.
        FileNotFoundError: If `catalog_path` does not exist (the
            bundled catalog ships with the wheel, so this only fires
            when a caller passes an explicit, missing path).
    """
    path = catalog_path if catalog_path is not None else CATALOG_PATH
    files = _yaml_files_for(path)
    try:
        mtime_tuple = tuple((str(f), f.stat().st_mtime_ns) for f in files)
    except FileNotFoundError:
        mtime_tuple = ((str(path.resolve()), 0),)
    cache_key = (str(path.resolve()), mtime_tuple)
    cached = _CATALOG_CACHE.get(cache_key)
    if cached is not None:
        return cached

    merged_rows: dict[str, dict[str, Any]] = {}
    row_origin: dict[str, Path] = {}
    available: list[str] = []
    for file_path in files:
        data = load_yaml_strict(file_path) or {}
        for ident in data.get("available_datasets") or []:
            available.append(str(ident))
        for key, body in (data.get("datasets") or {}).items():
            if key in merged_rows:
                raise ValueError(
                    f"dataset {key!r} declared in two catalog files: "
                    f"{row_origin[key]} and {file_path}"
                )
            merged_rows[key] = dict(body or {})
            row_origin[key] = file_path

    if not merged_rows:
        raise ValueError(
            f"{path} is missing or has an empty 'datasets:' block. "
            "The JAXA catalog must list at least one dataset."
        )
    datasets: dict[str, Dataset] = {}
    for key, body in merged_rows.items():
        try:
            datasets[key] = Dataset(key=key, **body)
        except ValidationError as exc:
            raise ValueError(
                f"{row_origin[key]} dataset {key!r} failed validation:\n{exc}"
            ) from exc
    catalog = cls(datasets=datasets, available_datasets=available)
    _CATALOG_CACHE[cache_key] = catalog
    return catalog

model_post_init(__context) #

Auto-load the bundled catalog when no datasets were supplied.

Catalog() with no args reads :data:CATALOG_PATH (cached by (path, mtime)); passing datasets=... skips the disk read (used in tests). Either way the aliases map is derived from the loaded rows. available_datasets is populated from the YAML's optional available_datasets: block (the refresh CLI's --write target — an informational index of every live SDK id across all three protocols) or, when that block is absent, defaults to the sorted curated keys so the dict-like surface (len(cat) etc.) still works.

Source code in src/earthlens/jaxa/catalog.py
def model_post_init(self, __context: Any) -> None:
    """Auto-load the bundled catalog when no datasets were supplied.

    `Catalog()` with no args reads :data:`CATALOG_PATH` (cached by
    `(path, mtime)`); passing `datasets=...` skips the disk read (used
    in tests). Either way the `aliases` map is derived from the loaded
    rows. `available_datasets` is populated from the YAML's optional
    `available_datasets:` block (the refresh CLI's `--write` target —
    an informational index of every live SDK id across all three
    protocols) or, when that block is absent, defaults to the sorted
    curated keys so the dict-like surface (`len(cat)` etc.) still
    works.
    """
    if not self.datasets:
        loaded = Catalog.load()
        self.datasets = loaded.datasets
        if loaded.available_datasets and not self.available_datasets:
            self.available_datasets = loaded.available_datasets
    # Build the alias index from the rows' `aliases` lists. Conflicts
    # (same alias on two rows) raise; canonical keys also resolve to
    # themselves so `resolve()` is total over both.
    aliases: dict[str, str] = {}
    for key, ds in self.datasets.items():
        aliases[key] = key
        for alias in ds.aliases:
            if alias in aliases and aliases[alias] != key:
                raise ValueError(
                    f"alias {alias!r} is claimed by both {aliases[alias]!r} "
                    f"and {key!r} — pick one."
                )
            aliases[alias] = key
    self.aliases = aliases
    if not self.available_datasets:
        self.available_datasets = sorted(self.datasets)

resolve(key) #

Map a friendly alias / raw id to its canonical key.

Parameters:

Name Type Description Default
key str

A canonical key, friendly alias, raw G-Portal numeric id (e.g. "11001002"), or raw jaxa.earth STAC collection name (e.g. "JAXA.EORC_ALOS.PRISM_AW3D30.v3.2_global").

required

Returns:

Name Type Description
str str

The canonical catalog key. Raw ids pass through unchanged.

Raises:

Type Description
ValueError

When key is neither curated nor a syntactically valid raw id, with a did-you-mean hint drawn from the union of canonical keys and aliases.

Examples:

  • A canonical key resolves to itself; an alias resolves to its row:
    >>> from earthlens.jaxa import Catalog
    >>> cat = Catalog()
    >>> cat.resolve("aw3d30")
    'aw3d30'
    >>> cat.resolve("elevation")
    'aw3d30'
    
  • Raw G-Portal numeric ids pass through unmapped:
    >>> from earthlens.jaxa import Catalog
    >>> Catalog().resolve("11001002")
    '11001002'
    
Source code in src/earthlens/jaxa/catalog.py
def resolve(self, key: str) -> str:
    """Map a friendly alias / raw id to its canonical key.

    Args:
        key: A canonical key, friendly alias, raw G-Portal numeric
            id (e.g. ``"11001002"``), or raw jaxa.earth STAC
            collection name (e.g. ``"JAXA.EORC_ALOS.PRISM_AW3D30.v3.2_global"``).

    Returns:
        str: The canonical catalog key. Raw ids pass through unchanged.

    Raises:
        ValueError: When `key` is neither curated nor a syntactically
            valid raw id, with a did-you-mean hint drawn from the
            union of canonical keys and aliases.

    Examples:
        - A canonical key resolves to itself; an alias resolves to its row:
            ```python
            >>> from earthlens.jaxa import Catalog
            >>> cat = Catalog()
            >>> cat.resolve("aw3d30")
            'aw3d30'
            >>> cat.resolve("elevation")
            'aw3d30'

            ```
        - Raw G-Portal numeric ids pass through unmapped:
            ```python
            >>> from earthlens.jaxa import Catalog
            >>> Catalog().resolve("11001002")
            '11001002'

            ```
    """
    if key in self.aliases:
        return self.aliases[key]
    # Raw G-Portal id (7-9 digits) or raw jaxa.earth STAC collection name
    # passes through unmapped, mirroring `usgs_water.Catalog.resolve` for
    # raw 5-digit NWIS codes. The available_datasets: index lets a
    # curator verify the id is real upstream.
    if _GPORTAL_ID_RE.match(key) or _JAXA_EARTH_PREFIX_RE.match(key):
        return key
    close = difflib.get_close_matches(key, self.aliases, n=1)
    hint = f" Did you mean {close[0]!r}?" if close else ""
    raise ValueError(
        f"{key!r} is not in the JAXA catalog. "
        f"Known keys: {sorted(self.datasets)}.{hint}"
    )

Dataset #

Bases: BaseModel

One JAXA dataset row.

The row's protocol field is the discriminator the backend dispatches on (_jaxa_earth.py vs _gportal.py vs _ptree.py). The cross-field validator enforces that the right identifier is present for each protocol:

  • protocol="jaxa-earth" requires collection (the STAC collection name the jaxa.earth API consumes, e.g. "JAXA.EORC_ALOS.PRISM_AW3D30.v3.2_global").
  • protocol="gportal" requires short_name (the G-Portal numeric dataset id, e.g. "10003001").
  • protocol="ptree" requires short_name (the P-Tree product token, e.g. "himawari-ahi-fldk").

Attributes:

Name Type Description
key str

Canonical catalog key ("aw3d30") — repeated on the row so it travels with the object outside the catalog dict.

protocol JaxaProtocol

One of "jaxa-earth", "gportal", or "ptree".

collection str | None

STAC collection name. Required when protocol="jaxa-earth"; must be None for gportal and ptree.

short_name str | None

Protocol-specific product identifier — a G-Portal numeric dataset id (e.g. "10003001") for gportal, or a P-Tree product token (e.g. "himawari-ahi-fldk") for ptree. Required for both credentialed protocols; must be None for jaxa-earth.

default_band str | None

The band selected when the user does not pass bands=. Used by jaxa-earth (picks the GeoTIFF band to write) and ptree (picks which Himawari AHI band to download from B01..B16). Ignored for gportal (G-Portal products are downloaded whole).

aliases list[str]

Friendly keys that resolve to this row's canonical key (e.g. ["elevation", "dem"] for "aw3d30").

description str

Human-readable summary used in docs.

Examples:

  • A jaxa-earth row needs collection:
    >>> from earthlens.jaxa.catalog import Dataset
    >>> ds = Dataset(
    ...     key="aw3d30",
    ...     protocol="jaxa-earth",
    ...     collection="JAXA.EORC_ALOS.PRISM_AW3D30.v3.2_global",
    ...     default_band="DSM",
    ... )
    >>> ds.protocol
    'jaxa-earth'
    
  • A gportal row needs short_name:
    >>> from earthlens.jaxa.catalog import Dataset
    >>> ds = Dataset(key="sgli-l380", protocol="gportal", short_name="10003001")
    >>> ds.short_name
    '10003001'
    
Source code in src/earthlens/jaxa/catalog.py
class Dataset(BaseModel):
    """One JAXA dataset row.

    The row's `protocol` field is the discriminator the backend dispatches
    on (`_jaxa_earth.py` vs `_gportal.py` vs `_ptree.py`). The cross-field
    validator enforces that the right identifier is present for each
    protocol:

    * `protocol="jaxa-earth"` requires `collection` (the STAC collection
      name the `jaxa.earth` API consumes, e.g.
      `"JAXA.EORC_ALOS.PRISM_AW3D30.v3.2_global"`).
    * `protocol="gportal"` requires `short_name` (the G-Portal numeric
      dataset id, e.g. `"10003001"`).
    * `protocol="ptree"` requires `short_name` (the P-Tree product
      token, e.g. `"himawari-ahi-fldk"`).

    Attributes:
        key: Canonical catalog key (`"aw3d30"`) — repeated on the row so it
            travels with the object outside the catalog dict.
        protocol: One of `"jaxa-earth"`, `"gportal"`, or `"ptree"`.
        collection: STAC collection name. Required when
            `protocol="jaxa-earth"`; must be `None` for `gportal` and
            `ptree`.
        short_name: Protocol-specific product identifier — a G-Portal
            numeric dataset id (e.g. `"10003001"`) for `gportal`, or a
            P-Tree product token (e.g. `"himawari-ahi-fldk"`) for
            `ptree`. Required for both credentialed protocols; must be
            `None` for `jaxa-earth`.
        default_band: The band selected when the user does not pass
            `bands=`. Used by `jaxa-earth` (picks the GeoTIFF band to
            write) and `ptree` (picks which Himawari AHI band to
            download from `B01`..`B16`). Ignored for `gportal`
            (G-Portal products are downloaded whole).
        aliases: Friendly keys that resolve to this row's canonical key
            (e.g. `["elevation", "dem"]` for `"aw3d30"`).
        description: Human-readable summary used in docs.

    Examples:
        - A `jaxa-earth` row needs `collection`:
            ```python
            >>> from earthlens.jaxa.catalog import Dataset
            >>> ds = Dataset(
            ...     key="aw3d30",
            ...     protocol="jaxa-earth",
            ...     collection="JAXA.EORC_ALOS.PRISM_AW3D30.v3.2_global",
            ...     default_band="DSM",
            ... )
            >>> ds.protocol
            'jaxa-earth'

            ```
        - A `gportal` row needs `short_name`:
            ```python
            >>> from earthlens.jaxa.catalog import Dataset
            >>> ds = Dataset(key="sgli-l380", protocol="gportal", short_name="10003001")
            >>> ds.short_name
            '10003001'

            ```
    """

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

    key: str
    protocol: JaxaProtocol
    collection: str | None = None
    short_name: str | None = None
    default_band: str | None = None
    aliases: list[str] = Field(default_factory=list)
    description: str = ""

    @model_validator(mode="after")
    def _check_protocol_identifier(self) -> Dataset:
        """Enforce one identifier per protocol.

        Raises:
            ValueError: If a `jaxa-earth` row has no `collection`, or a
                credentialed (`gportal` / `ptree`) row has no
                `short_name`, or the row sets the identifier belonging
                to the other side (a `jaxa-earth` row with a
                `short_name`, or a credentialed row with a
                `collection`).
        """
        if self.protocol == "jaxa-earth":
            if not self.collection:
                raise ValueError(
                    f"dataset {self.key!r} has protocol='jaxa-earth' but no "
                    "`collection` (the STAC collection name) — set it."
                )
            if self.short_name is not None:
                raise ValueError(
                    f"dataset {self.key!r} has protocol='jaxa-earth'; "
                    "`short_name` belongs to a credentialed protocol "
                    "(gportal/ptree) — drop it."
                )
        else:
            if not self.short_name:
                raise ValueError(
                    f"dataset {self.key!r} has protocol={self.protocol!r} "
                    "but no `short_name` (the protocol's product identifier) "
                    "— set it."
                )
            if self.collection is not None:
                raise ValueError(
                    f"dataset {self.key!r} has protocol={self.protocol!r}; "
                    "`collection` belongs to the jaxa-earth protocol "
                    "— drop it."
                )
        return self

JAXA #

Bases: AbstractDataSource

Unified JAXA backend over three protocols (jaxa-earth, gportal, ptree).

Attributes:

Name Type Description
OUTPUT_KIND OutputKind

Fixed "raster". The jaxa-earth branch always emits GeoTIFFs; the gportal branch writes raw products (HDF5, GeoTIFF, NetCDF — mission-dependent); the ptree branch writes raw Himawari HSD .DAT.bz2 segments (10 per band per 10-minute slot) — all three downstream readers still treat the output as gridded artefacts.

Source code in src/earthlens/jaxa/backend.py
class JAXA(AbstractDataSource):
    """Unified JAXA backend over three protocols (`jaxa-earth`, `gportal`, `ptree`).

    Attributes:
        OUTPUT_KIND: Fixed `"raster"`. The `jaxa-earth` branch always emits
            GeoTIFFs; the `gportal` branch writes raw products (HDF5,
            GeoTIFF, NetCDF — mission-dependent); the `ptree` branch
            writes raw Himawari HSD `.DAT.bz2` segments (10 per band per
            10-minute slot) — all three downstream readers still treat
            the output as gridded artefacts.
    """

    OUTPUT_KIND: OutputKind = "raster"

    def __init__(
        self,
        start: str,
        end: str,
        variables: list[str],
        lat_lim: list[float],
        lon_lim: list[float],
        temporal_resolution: str = "daily",
        path: Path | str = "",
        fmt: str = "%Y-%m-%d",
        *,
        resolution: float | None = None,
        bands: list[str] | None = None,
        gportal_username: str | None = None,
        gportal_password: str | None = None,
        ptree_username: str | None = None,
        ptree_password: str | None = None,
        catalog: Catalog | None = None,
    ):
        """Initialise a JAXA backend instance.

        Resolves every key against the catalog up front so that an unknown
        key (or a request that mixes more than one of the three protocols)
        fails at construction rather than mid-download.

        Args:
            start: Inclusive start of the date window (parsed with `fmt`).
            end: Inclusive end of the date window.
            variables: Catalog dataset keys, canonical or alias. Every key
                must resolve to the **same** protocol — mixing keys from
                more than one of `jaxa-earth`, `gportal`, and `ptree`
                raises with a diagnostic message that names each violated
                protocol and its offending keys.
            lat_lim: `[lat_min, lat_max]` in degrees.
            lon_lim: `[lon_min, lon_max]` in degrees.
            temporal_resolution: Advisory label only — JAXA Earth uses
                the full date range, G-Portal queries on
                `(start, end)`, and `_ptree.fetch_ptree` iterates every
                10-minute slot in the window.
            path: Output directory (created if missing).
            fmt: `strptime` format for `start` / `end`.
            resolution: `ppu` (pixels per degree) for the `jaxa-earth`
                branch. `None` lets the API pick its native resolution.
                Ignored for `gportal` and `ptree`.
            bands: Override the catalog's `default_band`. For
                `jaxa-earth`, picks the band(s) whose GeoTIFF the
                backend writes. For `ptree`, selects which Himawari
                AHI band(s) to download from the full 10-segment set
                — `"B01"`..`"B16"` are valid, with `"B03"` (0.5 km
                visible) and `"B13"` (2 km IR) as the two common
                defaults. `None` uses the row's `default_band`.
                Ignored for `gportal` (products are downloaded whole).
            gportal_username: Explicit G-Portal username. When omitted,
                `JaxaAuth.configure()` reads `$GPORTAL_USERNAME` at
                authentication time. Only used by the `gportal` branch.
            gportal_password: Explicit G-Portal password. When omitted,
                falls back to `$GPORTAL_PASSWORD`. Only used by the
                `gportal` branch.
            ptree_username: Explicit P-Tree username (the email
                registered at eorc.jaxa.jp/ptree). When omitted, falls
                back to `$JAXA_PTREE_USERNAME`. Only used by the
                `ptree` branch. Never reuse the G-Portal credentials —
                the two accounts are distinct.
            ptree_password: Explicit P-Tree password. When omitted,
                falls back to `$JAXA_PTREE_PASSWORD`. Only used by the
                `ptree` branch.
            catalog: Optional pre-built `Catalog` (tests inject a faked
                one); defaults to the bundled catalog.

        Raises:
            ValueError: If `variables` is empty, an alias is unknown, or
                the resolved keys span more than one protocol.
        """
        if not variables:
            raise ValueError(
                "JAXA requires a non-empty `variables` list of dataset keys, "
                'e.g. ["aw3d30"] or ["elevation"].'
            )

        self._catalog = catalog if catalog is not None else Catalog()
        self._resolution = resolution
        self._bands_override = bands

        resolved: list[Dataset] = [self._catalog.get(v) for v in variables]
        protocols = {ds.protocol for ds in resolved}
        if len(protocols) > 1:
            groups = {
                p: [ds.key for ds in resolved if ds.protocol == p]
                for p in sorted(protocols)
            }
            summary = ", ".join(f"{p}={ks}" for p, ks in groups.items())
            raise ValueError(
                "one JAXA request must target a single protocol, but the "
                f"requested variables span multiple: {summary}. "
                "Issue one JAXA(...) call per protocol."
            )
        self._resolved: list[Dataset] = resolved
        self._protocol: JaxaProtocol = next(iter(protocols))

        creds = JaxaCredentials(
            gportal_username=gportal_username,
            gportal_password=SecretStr(gportal_password)
            if gportal_password is not None
            else None,
            ptree_username=ptree_username,
            ptree_password=SecretStr(ptree_password)
            if ptree_password is not None
            else None,
        )
        # Bind the protocol so `AbstractDataSource.authenticate()` (which
        # calls `self._auth.configure()` with no args) fails fast on
        # missing credentials for whichever credentialed branch this
        # request pins (`gportal` or `ptree`); jaxa-earth is authless.
        self._auth = JaxaAuth(creds, protocol=self._protocol)

        super().__init__(
            start=start,
            end=end,
            variables=variables,
            temporal_resolution=temporal_resolution,
            lat_lim=lat_lim,
            lon_lim=lon_lim,
            fmt=fmt,
            path=path,
        )

    @property
    def auth(self) -> JaxaAuth:
        """The :class:`JaxaAuth` bound to this request.

        Returns:
            JaxaAuth: The optional-credentials auth object. `configure()`
            is a no-op for the `jaxa-earth` protocol, resolves
            G-Portal credentials for `gportal`, and resolves P-Tree
            credentials for `ptree`.

        Examples:
            - The auth object's protocol always matches the backend's:
                ```python
                >>> import tempfile
                >>> from earthlens.jaxa import JAXA
                >>> backend = JAXA(
                ...     start="2020-01-01", end="2020-12-31",
                ...     variables=["aw3d30"],
                ...     lat_lim=[35.0, 36.0], lon_lim=[138.0, 139.0],
                ...     path=tempfile.gettempdir(),
                ... )
                >>> backend.auth.protocol
                'jaxa-earth'

                ```
        """
        return self._auth

    @property
    def protocol(self) -> JaxaProtocol:
        """The single protocol this request resolved to.

        Returns:
            JaxaProtocol: One of `"jaxa-earth"`, `"gportal"`, or
                `"ptree"`.

        Examples:
            - Resolving an alias pins the protocol on construction:
                ```python
                >>> import tempfile
                >>> from earthlens.jaxa import JAXA
                >>> JAXA(
                ...     start="2020-01-01", end="2020-12-31",
                ...     variables=["elevation"],
                ...     lat_lim=[35.0, 36.0], lon_lim=[138.0, 139.0],
                ...     path=tempfile.gettempdir(),
                ... ).protocol
                'jaxa-earth'

                ```
        """
        return self._protocol

    def _initialize(self) -> None:
        """No eager connection setup.

        All three branches import their transport clients lazily:
        `jaxa.earth` and `gportal` inside their branch modules (needing
        the `[jaxa]` extra), and the `ptree` branch uses only stdlib
        `ftplib` on demand. `jaxa-earth` is authless; the optional
        `gportal` / `ptree` credentials are resolved lazily —
        `download()` calls `self._auth.configure()` before dispatching,
        and so does the explicit `EarthLens(...).authenticate()`
        entry point.

        Returns:
            None: No backend client to attach.
        """
        return None

    def _create_grid(self, lat_lim: list, lon_lim: list) -> SpatialExtent:
        """Wrap the user bbox into a `SpatialExtent`.

        Args:
            lat_lim: `[lat_min, lat_max]` in degrees.
            lon_lim: `[lon_min, lon_max]` in degrees.

        Returns:
            SpatialExtent: Validated, frozen bbox (WGS84).
        """
        return SpatialExtent.from_pairs(lat_lim=lat_lim, lon_lim=lon_lim)

    def _check_input_dates(
        self,
        start: str,
        end: str,
        temporal_resolution: str,
        fmt: str,
    ) -> TemporalExtent:
        """Parse the date window into a `TemporalExtent`.

        The frequency is advisory: `jaxa-earth` consumes a `dlim` range
        directly, `gportal.search` consumes `start_time` / `end_time`
        ISO strings, and `_ptree.fetch_ptree` iterates every 10-minute
        slot in the window.

        Args:
            start: Inclusive start of the window.
            end: Inclusive end of the window.
            temporal_resolution: Advisory label; defaults to `"daily"`.
            fmt: `strptime` format applied to `start` / `end`.

        Returns:
            TemporalExtent: Frozen model with the parsed bounds.

        Raises:
            ValueError: If `start` parses to a date later than `end`.
        """
        start_dt = dt.datetime.strptime(start, fmt)
        end_dt = dt.datetime.strptime(end, fmt)
        freq_alias = _FREQ_ALIAS.get(temporal_resolution, "MS")
        dates = pd.date_range(start_dt, end_dt, freq=freq_alias)
        return TemporalExtent(
            start_date=start_dt,
            end_date=end_dt,
            resolution=freq_alias,
            dates=dates,
        )

    def _api(self) -> list[Path]:
        """Dispatch to the matching protocol branch.

        Returns:
            list[Path]: One or more written paths per resolved dataset
                — a COG for `jaxa-earth`, an HDF5 / GeoTIFF / NetCDF
                product for `gportal` (depending on the mission), or
                the 10 raw `.DAT.bz2` HSD segments per band per
                10-minute slot for `ptree`.
        """
        out_dir = Path(self.path)
        self._auth.configure()
        written: list[Path] = []
        if self._protocol == "jaxa-earth":
            from earthlens.jaxa._jaxa_earth import fetch_jaxa_earth

            for ds in self._resolved:
                written.extend(
                    fetch_jaxa_earth(
                        dataset=ds,
                        space=self.space,
                        time=self.time,
                        resolution=self._resolution,
                        bands=self._bands_override,
                        out_dir=out_dir,
                    )
                )
        elif self._protocol == "gportal":
            from earthlens.jaxa._gportal import fetch_gportal

            for ds in self._resolved:
                written.extend(
                    fetch_gportal(
                        dataset=ds,
                        space=self.space,
                        time=self.time,
                        auth=self._auth,
                        out_dir=out_dir,
                    )
                )
        elif self._protocol == "ptree":
            from earthlens.jaxa._ptree import fetch_ptree

            for ds in self._resolved:
                written.extend(
                    fetch_ptree(
                        dataset=ds,
                        space=self.space,
                        time=self.time,
                        auth=self._auth,
                        out_dir=out_dir,
                        bands=self._bands_override,
                    )
                )
        else:
            # Defensive: `JaxaProtocol` is a `Literal[...]` and the
            # constructor rejects anything else, so this only fires if a
            # future fourth protocol is added to the literal but not to
            # `_api`. Failing loud beats silently routing to the last
            # `else` branch (Round 2 L3).
            raise ValueError(
                f"unhandled JAXA protocol {self._protocol!r}; add a "
                "dispatch branch alongside jaxa-earth/gportal/ptree."
            )
        return written

    def download(
        self,
        progress_bar: bool = True,
        aggregate: AggregationConfig | None = None,
    ) -> list[Path]:
        """Fetch the requested datasets and return the written paths.

        Args:
            progress_bar: Reserved for future per-download progress;
                each branch prints its own progress lines today
                (jaxa-earth via `jaxa.earth`, gportal via the `gportal`
                SDK, ptree via one segment-path write per FTP transfer).
            aggregate: Not supported — JAXA requests return per-date
                rasters; reducing them across dates is a follow-on.

        Returns:
            list[Path]: One or more files per resolved dataset, in
                request order.

        Raises:
            NotImplementedError: When `aggregate` is non-`None`. See `G6`
                in the planning doc.

        Examples:
            - Passing `aggregate=` raises because per-date stacks are not
              reduced yet:
                ```python
                >>> import tempfile
                >>> from earthlens.jaxa import JAXA
                >>> backend = JAXA(
                ...     start="2020-01-01", end="2020-12-31",
                ...     variables=["aw3d30"],
                ...     lat_lim=[35.0, 36.0], lon_lim=[138.0, 139.0],
                ...     path=tempfile.gettempdir(),
                ... )
                >>> backend.download(aggregate=object())  # doctest: +ELLIPSIS
                Traceback (most recent call last):
                    ...
                NotImplementedError: JAXA does not yet support the aggregate=...

                ```
        """
        if aggregate is not None:
            raise NotImplementedError(
                "JAXA does not yet support the aggregate= argument. "
                "All three branches emit per-date artefacts today "
                "(jaxa-earth: per-date COGs; gportal: per-product "
                "SFTP downloads; ptree: per-slot HSD segments) — "
                "reducing them across dates is a planned follow-on "
                "(planning G6)."
            )
        del progress_bar
        return self._api()

auth property #

The :class:JaxaAuth bound to this request.

Returns:

Name Type Description
JaxaAuth JaxaAuth

The optional-credentials auth object. configure()

JaxaAuth

is a no-op for the jaxa-earth protocol, resolves

JaxaAuth

G-Portal credentials for gportal, and resolves P-Tree

JaxaAuth

credentials for ptree.

Examples:

  • The auth object's protocol always matches the backend's:
    >>> import tempfile
    >>> from earthlens.jaxa import JAXA
    >>> backend = JAXA(
    ...     start="2020-01-01", end="2020-12-31",
    ...     variables=["aw3d30"],
    ...     lat_lim=[35.0, 36.0], lon_lim=[138.0, 139.0],
    ...     path=tempfile.gettempdir(),
    ... )
    >>> backend.auth.protocol
    'jaxa-earth'
    

protocol property #

The single protocol this request resolved to.

Returns:

Name Type Description
JaxaProtocol JaxaProtocol

One of "jaxa-earth", "gportal", or "ptree".

Examples:

  • Resolving an alias pins the protocol on construction:
    >>> import tempfile
    >>> from earthlens.jaxa import JAXA
    >>> JAXA(
    ...     start="2020-01-01", end="2020-12-31",
    ...     variables=["elevation"],
    ...     lat_lim=[35.0, 36.0], lon_lim=[138.0, 139.0],
    ...     path=tempfile.gettempdir(),
    ... ).protocol
    'jaxa-earth'
    

__init__(start, end, variables, lat_lim, lon_lim, temporal_resolution='daily', path='', fmt='%Y-%m-%d', *, resolution=None, bands=None, gportal_username=None, gportal_password=None, ptree_username=None, ptree_password=None, catalog=None) #

Initialise a JAXA backend instance.

Resolves every key against the catalog up front so that an unknown key (or a request that mixes more than one of the three protocols) fails at construction rather than mid-download.

Parameters:

Name Type Description Default
start str

Inclusive start of the date window (parsed with fmt).

required
end str

Inclusive end of the date window.

required
variables list[str]

Catalog dataset keys, canonical or alias. Every key must resolve to the same protocol — mixing keys from more than one of jaxa-earth, gportal, and ptree raises with a diagnostic message that names each violated protocol and its offending keys.

required
lat_lim list[float]

[lat_min, lat_max] in degrees.

required
lon_lim list[float]

[lon_min, lon_max] in degrees.

required
temporal_resolution str

Advisory label only — JAXA Earth uses the full date range, G-Portal queries on (start, end), and _ptree.fetch_ptree iterates every 10-minute slot in the window.

'daily'
path Path | str

Output directory (created if missing).

''
fmt str

strptime format for start / end.

'%Y-%m-%d'
resolution float | None

ppu (pixels per degree) for the jaxa-earth branch. None lets the API pick its native resolution. Ignored for gportal and ptree.

None
bands list[str] | None

Override the catalog's default_band. For jaxa-earth, picks the band(s) whose GeoTIFF the backend writes. For ptree, selects which Himawari AHI band(s) to download from the full 10-segment set — "B01".."B16" are valid, with "B03" (0.5 km visible) and "B13" (2 km IR) as the two common defaults. None uses the row's default_band. Ignored for gportal (products are downloaded whole).

None
gportal_username str | None

Explicit G-Portal username. When omitted, JaxaAuth.configure() reads $GPORTAL_USERNAME at authentication time. Only used by the gportal branch.

None
gportal_password str | None

Explicit G-Portal password. When omitted, falls back to $GPORTAL_PASSWORD. Only used by the gportal branch.

None
ptree_username str | None

Explicit P-Tree username (the email registered at eorc.jaxa.jp/ptree). When omitted, falls back to $JAXA_PTREE_USERNAME. Only used by the ptree branch. Never reuse the G-Portal credentials — the two accounts are distinct.

None
ptree_password str | None

Explicit P-Tree password. When omitted, falls back to $JAXA_PTREE_PASSWORD. Only used by the ptree branch.

None
catalog Catalog | None

Optional pre-built Catalog (tests inject a faked one); defaults to the bundled catalog.

None

Raises:

Type Description
ValueError

If variables is empty, an alias is unknown, or the resolved keys span more than one protocol.

Source code in src/earthlens/jaxa/backend.py
def __init__(
    self,
    start: str,
    end: str,
    variables: list[str],
    lat_lim: list[float],
    lon_lim: list[float],
    temporal_resolution: str = "daily",
    path: Path | str = "",
    fmt: str = "%Y-%m-%d",
    *,
    resolution: float | None = None,
    bands: list[str] | None = None,
    gportal_username: str | None = None,
    gportal_password: str | None = None,
    ptree_username: str | None = None,
    ptree_password: str | None = None,
    catalog: Catalog | None = None,
):
    """Initialise a JAXA backend instance.

    Resolves every key against the catalog up front so that an unknown
    key (or a request that mixes more than one of the three protocols)
    fails at construction rather than mid-download.

    Args:
        start: Inclusive start of the date window (parsed with `fmt`).
        end: Inclusive end of the date window.
        variables: Catalog dataset keys, canonical or alias. Every key
            must resolve to the **same** protocol — mixing keys from
            more than one of `jaxa-earth`, `gportal`, and `ptree`
            raises with a diagnostic message that names each violated
            protocol and its offending keys.
        lat_lim: `[lat_min, lat_max]` in degrees.
        lon_lim: `[lon_min, lon_max]` in degrees.
        temporal_resolution: Advisory label only — JAXA Earth uses
            the full date range, G-Portal queries on
            `(start, end)`, and `_ptree.fetch_ptree` iterates every
            10-minute slot in the window.
        path: Output directory (created if missing).
        fmt: `strptime` format for `start` / `end`.
        resolution: `ppu` (pixels per degree) for the `jaxa-earth`
            branch. `None` lets the API pick its native resolution.
            Ignored for `gportal` and `ptree`.
        bands: Override the catalog's `default_band`. For
            `jaxa-earth`, picks the band(s) whose GeoTIFF the
            backend writes. For `ptree`, selects which Himawari
            AHI band(s) to download from the full 10-segment set
            — `"B01"`..`"B16"` are valid, with `"B03"` (0.5 km
            visible) and `"B13"` (2 km IR) as the two common
            defaults. `None` uses the row's `default_band`.
            Ignored for `gportal` (products are downloaded whole).
        gportal_username: Explicit G-Portal username. When omitted,
            `JaxaAuth.configure()` reads `$GPORTAL_USERNAME` at
            authentication time. Only used by the `gportal` branch.
        gportal_password: Explicit G-Portal password. When omitted,
            falls back to `$GPORTAL_PASSWORD`. Only used by the
            `gportal` branch.
        ptree_username: Explicit P-Tree username (the email
            registered at eorc.jaxa.jp/ptree). When omitted, falls
            back to `$JAXA_PTREE_USERNAME`. Only used by the
            `ptree` branch. Never reuse the G-Portal credentials —
            the two accounts are distinct.
        ptree_password: Explicit P-Tree password. When omitted,
            falls back to `$JAXA_PTREE_PASSWORD`. Only used by the
            `ptree` branch.
        catalog: Optional pre-built `Catalog` (tests inject a faked
            one); defaults to the bundled catalog.

    Raises:
        ValueError: If `variables` is empty, an alias is unknown, or
            the resolved keys span more than one protocol.
    """
    if not variables:
        raise ValueError(
            "JAXA requires a non-empty `variables` list of dataset keys, "
            'e.g. ["aw3d30"] or ["elevation"].'
        )

    self._catalog = catalog if catalog is not None else Catalog()
    self._resolution = resolution
    self._bands_override = bands

    resolved: list[Dataset] = [self._catalog.get(v) for v in variables]
    protocols = {ds.protocol for ds in resolved}
    if len(protocols) > 1:
        groups = {
            p: [ds.key for ds in resolved if ds.protocol == p]
            for p in sorted(protocols)
        }
        summary = ", ".join(f"{p}={ks}" for p, ks in groups.items())
        raise ValueError(
            "one JAXA request must target a single protocol, but the "
            f"requested variables span multiple: {summary}. "
            "Issue one JAXA(...) call per protocol."
        )
    self._resolved: list[Dataset] = resolved
    self._protocol: JaxaProtocol = next(iter(protocols))

    creds = JaxaCredentials(
        gportal_username=gportal_username,
        gportal_password=SecretStr(gportal_password)
        if gportal_password is not None
        else None,
        ptree_username=ptree_username,
        ptree_password=SecretStr(ptree_password)
        if ptree_password is not None
        else None,
    )
    # Bind the protocol so `AbstractDataSource.authenticate()` (which
    # calls `self._auth.configure()` with no args) fails fast on
    # missing credentials for whichever credentialed branch this
    # request pins (`gportal` or `ptree`); jaxa-earth is authless.
    self._auth = JaxaAuth(creds, protocol=self._protocol)

    super().__init__(
        start=start,
        end=end,
        variables=variables,
        temporal_resolution=temporal_resolution,
        lat_lim=lat_lim,
        lon_lim=lon_lim,
        fmt=fmt,
        path=path,
    )

download(progress_bar=True, aggregate=None) #

Fetch the requested datasets and return the written paths.

Parameters:

Name Type Description Default
progress_bar bool

Reserved for future per-download progress; each branch prints its own progress lines today (jaxa-earth via jaxa.earth, gportal via the gportal SDK, ptree via one segment-path write per FTP transfer).

True
aggregate AggregationConfig | None

Not supported — JAXA requests return per-date rasters; reducing them across dates is a follow-on.

None

Returns:

Type Description
list[Path]

list[Path]: One or more files per resolved dataset, in request order.

Raises:

Type Description
NotImplementedError

When aggregate is non-None. See G6 in the planning doc.

Examples:

  • Passing aggregate= raises because per-date stacks are not reduced yet:
    >>> import tempfile
    >>> from earthlens.jaxa import JAXA
    >>> backend = JAXA(
    ...     start="2020-01-01", end="2020-12-31",
    ...     variables=["aw3d30"],
    ...     lat_lim=[35.0, 36.0], lon_lim=[138.0, 139.0],
    ...     path=tempfile.gettempdir(),
    ... )
    >>> backend.download(aggregate=object())  # doctest: +ELLIPSIS
    Traceback (most recent call last):
        ...
    NotImplementedError: JAXA does not yet support the aggregate=...
    
Source code in src/earthlens/jaxa/backend.py
def download(
    self,
    progress_bar: bool = True,
    aggregate: AggregationConfig | None = None,
) -> list[Path]:
    """Fetch the requested datasets and return the written paths.

    Args:
        progress_bar: Reserved for future per-download progress;
            each branch prints its own progress lines today
            (jaxa-earth via `jaxa.earth`, gportal via the `gportal`
            SDK, ptree via one segment-path write per FTP transfer).
        aggregate: Not supported — JAXA requests return per-date
            rasters; reducing them across dates is a follow-on.

    Returns:
        list[Path]: One or more files per resolved dataset, in
            request order.

    Raises:
        NotImplementedError: When `aggregate` is non-`None`. See `G6`
            in the planning doc.

    Examples:
        - Passing `aggregate=` raises because per-date stacks are not
          reduced yet:
            ```python
            >>> import tempfile
            >>> from earthlens.jaxa import JAXA
            >>> backend = JAXA(
            ...     start="2020-01-01", end="2020-12-31",
            ...     variables=["aw3d30"],
            ...     lat_lim=[35.0, 36.0], lon_lim=[138.0, 139.0],
            ...     path=tempfile.gettempdir(),
            ... )
            >>> backend.download(aggregate=object())  # doctest: +ELLIPSIS
            Traceback (most recent call last):
                ...
            NotImplementedError: JAXA does not yet support the aggregate=...

            ```
    """
    if aggregate is not None:
        raise NotImplementedError(
            "JAXA does not yet support the aggregate= argument. "
            "All three branches emit per-date artefacts today "
            "(jaxa-earth: per-date COGs; gportal: per-product "
            "SFTP downloads; ptree: per-slot HSD segments) — "
            "reducing them across dates is a planned follow-on "
            "(planning G6)."
        )
    del progress_bar
    return self._api()

JaxaAuth #

Bases: AbstractAuth[JaxaCredentials]

Resolve credentials for the bound JAXA protocol.

The class is optional-credentials: the protocol is bound at construction so the parent contract's no-arg configure() / is_authenticated() (the methods AbstractDataSource.authenticate() calls) act on the right side. JaxaAuth(creds, protocol="jaxa-earth") makes configure() a no-op; JaxaAuth(creds, protocol="gportal") or JaxaAuth(creds, protocol="ptree") resolves the matching username + password from explicit credentials or the protocol's env-var pair, raising :class:AuthenticationError on miss.

After a successful configure() on a credentialed protocol, the resolved values are available via :attr:username and :attr:password so the branch can pass them straight into its transport client (gportal.download(username=, password=), ftplib.FTP.login(...), paramiko.Transport.connect(...)) instead of mutating any SDK's module-level globals.

Attributes:

Name Type Description
_creds

The :class:JaxaCredentials passed at construction.

_protocol JaxaProtocol

The bound protocol — one of "jaxa-earth", "gportal" or "ptree".

Examples:

  • The jaxa-earth protocol never needs credentials:
    >>> from earthlens.jaxa import JaxaAuth, JaxaCredentials
    >>> auth = JaxaAuth(JaxaCredentials(), protocol="jaxa-earth")
    >>> auth.configure()
    >>> auth.is_authenticated()
    True
    
Source code in src/earthlens/jaxa/auth.py
class JaxaAuth(AbstractAuth[JaxaCredentials]):
    """Resolve credentials for the bound JAXA protocol.

    The class is optional-credentials: the protocol is **bound at
    construction** so the parent contract's no-arg `configure()` /
    `is_authenticated()` (the methods `AbstractDataSource.authenticate()`
    calls) act on the right side. `JaxaAuth(creds, protocol="jaxa-earth")`
    makes `configure()` a no-op; `JaxaAuth(creds, protocol="gportal")` or
    `JaxaAuth(creds, protocol="ptree")` resolves the matching username +
    password from explicit credentials or the protocol's env-var pair,
    raising :class:`AuthenticationError` on miss.

    After a successful `configure()` on a credentialed protocol, the
    resolved values are available via :attr:`username` and
    :attr:`password` so the branch can pass them straight into its
    transport client (`gportal.download(username=, password=)`,
    `ftplib.FTP.login(...)`, `paramiko.Transport.connect(...)`) instead of
    mutating any SDK's module-level globals.

    Attributes:
        _creds: The :class:`JaxaCredentials` passed at construction.
        _protocol: The bound protocol — one of `"jaxa-earth"`, `"gportal"`
            or `"ptree"`.

    Examples:
        - The `jaxa-earth` protocol never needs credentials:
            ```python
            >>> from earthlens.jaxa import JaxaAuth, JaxaCredentials
            >>> auth = JaxaAuth(JaxaCredentials(), protocol="jaxa-earth")
            >>> auth.configure()
            >>> auth.is_authenticated()
            True

            ```
    """

    def __init__(
        self,
        credentials: JaxaCredentials,
        protocol: JaxaProtocol = "jaxa-earth",
    ) -> None:
        """Bind the protocol; does not resolve credentials yet.

        Args:
            credentials: The :class:`JaxaCredentials` value object carrying
                the optional G-Portal and P-Tree username / password pairs.
            protocol: The protocol this auth instance targets. The
                parent's no-arg :meth:`configure` dispatches on this so
                `AbstractDataSource.authenticate()` fails fast for missing
                credentials on either credentialed branch.

        Raises:
            ValueError: When `protocol` is not one of the three supported
                values.
        """
        if protocol not in ("jaxa-earth", "gportal", "ptree"):
            raise ValueError(
                "protocol must be one of 'jaxa-earth', 'gportal', 'ptree'; "
                f"got {protocol!r}."
            )
        super().__init__(credentials)
        self._protocol: JaxaProtocol = protocol
        self._configured = False
        self._username: str | None = None
        self._password: SecretStr | None = None

    @property
    def protocol(self) -> JaxaProtocol:
        """The protocol bound at construction."""
        return self._protocol

    @property
    def username(self) -> str | None:
        """The resolved username, or `None` until `configure()` runs."""
        return self._username

    @property
    def password(self) -> SecretStr | None:
        """The resolved password (still wrapped in `SecretStr`).

        Returns `None` until :meth:`configure` runs. Callers that need
        the raw string call `.get_secret_value()` themselves at the call
        site — the wrapper stays on `JaxaAuth` so it never lands in a
        `repr` or a log line.
        """
        return self._password

    def configure(self) -> None:
        """Resolve credentials for the bound protocol.

        For `"jaxa-earth"` the call is a no-op (the JAXA Earth API needs
        no auth). For `"gportal"` and `"ptree"` the call reads the
        explicit credentials (preferred) or the protocol's environment
        variables (`$GPORTAL_USERNAME` / `$GPORTAL_PASSWORD` and
        `$JAXA_PTREE_USERNAME` / `$JAXA_PTREE_PASSWORD` respectively) as
        fallback, and caches them on the instance for the branch to read
        through :attr:`username` / :attr:`password`. Idempotent — a
        second call after `is_authenticated()` returns `True`
        short-circuits.

        Raises:
            AuthenticationError: When the protocol is credentialed and
                neither the explicit credentials nor the environment
                variables supply a usable username + password pair. The
                message names the env vars and the free-registration URL
                for the protocol that failed to resolve.

        Examples:
            - The jaxa-earth protocol's configure is a no-op:
                ```python
                >>> from earthlens.jaxa import JaxaAuth, JaxaCredentials
                >>> auth = JaxaAuth(JaxaCredentials(), protocol="jaxa-earth")
                >>> auth.configure()
                >>> auth.is_authenticated()
                True

                ```
            - With gportal credentials supplied explicitly, configure caches them
              for the branch to read via `.username` / `.password`:
                ```python
                >>> from pydantic import SecretStr
                >>> from earthlens.jaxa import JaxaAuth, JaxaCredentials
                >>> creds = JaxaCredentials(
                ...     gportal_username="alice",
                ...     gportal_password=SecretStr("topsecret"),
                ... )
                >>> auth = JaxaAuth(creds, protocol="gportal")
                >>> auth.configure()
                >>> auth.username
                'alice'

                ```
            - The `ptree` protocol reads its own credential pair:
                ```python
                >>> from pydantic import SecretStr
                >>> from earthlens.jaxa import JaxaAuth, JaxaCredentials
                >>> creds = JaxaCredentials(
                ...     ptree_username="alice@example.org",
                ...     ptree_password=SecretStr("hunter2"),
                ... )
                >>> auth = JaxaAuth(creds, protocol="ptree")
                >>> auth.configure()
                >>> auth.username
                'alice@example.org'

                ```
        """
        if self._configured:
            return
        if self._protocol == "jaxa-earth":
            self._configured = True
            return
        spec = _PROTOCOL_SPECS[self._protocol]
        cred_username = getattr(self._creds, spec.cred_user)
        cred_password: SecretStr | None = getattr(self._creds, spec.cred_pass)
        username = cred_username or os.environ.get(spec.env_user)
        password_raw = (
            cred_password.get_secret_value()
            if cred_password is not None
            else os.environ.get(spec.env_pass)
        )
        if not username or not password_raw:
            raise AuthenticationError(
                f"no {spec.human_name} credentials available: pass "
                f"{spec.cred_user}= and {spec.cred_pass}= to JAXA(...), "
                f"or set both {spec.env_user} and {spec.env_pass} "
                f"environment variables. Register a free account at "
                f"{spec.register_url}."
            )
        self._username = username
        self._password = SecretStr(password_raw)
        self._configured = True

    def is_authenticated(self) -> bool:
        """Return `True` once :meth:`configure` has run successfully.

        Returns:
            bool: `True` after :meth:`configure` has completed without
                raising; `False` before then.

        Examples:
            - Construction does not authenticate; configure flips the flag:
                ```python
                >>> from earthlens.jaxa import JaxaAuth, JaxaCredentials
                >>> auth = JaxaAuth(JaxaCredentials(), protocol="jaxa-earth")
                >>> auth.is_authenticated()
                False
                >>> auth.configure()
                >>> auth.is_authenticated()
                True

                ```
        """
        return self._configured

password property #

The resolved password (still wrapped in SecretStr).

Returns None until :meth:configure runs. Callers that need the raw string call .get_secret_value() themselves at the call site — the wrapper stays on JaxaAuth so it never lands in a repr or a log line.

protocol property #

The protocol bound at construction.

username property #

The resolved username, or None until configure() runs.

__init__(credentials, protocol='jaxa-earth') #

Bind the protocol; does not resolve credentials yet.

Parameters:

Name Type Description Default
credentials JaxaCredentials

The :class:JaxaCredentials value object carrying the optional G-Portal and P-Tree username / password pairs.

required
protocol JaxaProtocol

The protocol this auth instance targets. The parent's no-arg :meth:configure dispatches on this so AbstractDataSource.authenticate() fails fast for missing credentials on either credentialed branch.

'jaxa-earth'

Raises:

Type Description
ValueError

When protocol is not one of the three supported values.

Source code in src/earthlens/jaxa/auth.py
def __init__(
    self,
    credentials: JaxaCredentials,
    protocol: JaxaProtocol = "jaxa-earth",
) -> None:
    """Bind the protocol; does not resolve credentials yet.

    Args:
        credentials: The :class:`JaxaCredentials` value object carrying
            the optional G-Portal and P-Tree username / password pairs.
        protocol: The protocol this auth instance targets. The
            parent's no-arg :meth:`configure` dispatches on this so
            `AbstractDataSource.authenticate()` fails fast for missing
            credentials on either credentialed branch.

    Raises:
        ValueError: When `protocol` is not one of the three supported
            values.
    """
    if protocol not in ("jaxa-earth", "gportal", "ptree"):
        raise ValueError(
            "protocol must be one of 'jaxa-earth', 'gportal', 'ptree'; "
            f"got {protocol!r}."
        )
    super().__init__(credentials)
    self._protocol: JaxaProtocol = protocol
    self._configured = False
    self._username: str | None = None
    self._password: SecretStr | None = None

configure() #

Resolve credentials for the bound protocol.

For "jaxa-earth" the call is a no-op (the JAXA Earth API needs no auth). For "gportal" and "ptree" the call reads the explicit credentials (preferred) or the protocol's environment variables ($GPORTAL_USERNAME / $GPORTAL_PASSWORD and $JAXA_PTREE_USERNAME / $JAXA_PTREE_PASSWORD respectively) as fallback, and caches them on the instance for the branch to read through :attr:username / :attr:password. Idempotent — a second call after is_authenticated() returns True short-circuits.

Raises:

Type Description
AuthenticationError

When the protocol is credentialed and neither the explicit credentials nor the environment variables supply a usable username + password pair. The message names the env vars and the free-registration URL for the protocol that failed to resolve.

Examples:

  • The jaxa-earth protocol's configure is a no-op:
    >>> from earthlens.jaxa import JaxaAuth, JaxaCredentials
    >>> auth = JaxaAuth(JaxaCredentials(), protocol="jaxa-earth")
    >>> auth.configure()
    >>> auth.is_authenticated()
    True
    
  • With gportal credentials supplied explicitly, configure caches them for the branch to read via .username / .password:
    >>> from pydantic import SecretStr
    >>> from earthlens.jaxa import JaxaAuth, JaxaCredentials
    >>> creds = JaxaCredentials(
    ...     gportal_username="alice",
    ...     gportal_password=SecretStr("topsecret"),
    ... )
    >>> auth = JaxaAuth(creds, protocol="gportal")
    >>> auth.configure()
    >>> auth.username
    'alice'
    
  • The ptree protocol reads its own credential pair:
    >>> from pydantic import SecretStr
    >>> from earthlens.jaxa import JaxaAuth, JaxaCredentials
    >>> creds = JaxaCredentials(
    ...     ptree_username="alice@example.org",
    ...     ptree_password=SecretStr("hunter2"),
    ... )
    >>> auth = JaxaAuth(creds, protocol="ptree")
    >>> auth.configure()
    >>> auth.username
    'alice@example.org'
    
Source code in src/earthlens/jaxa/auth.py
def configure(self) -> None:
    """Resolve credentials for the bound protocol.

    For `"jaxa-earth"` the call is a no-op (the JAXA Earth API needs
    no auth). For `"gportal"` and `"ptree"` the call reads the
    explicit credentials (preferred) or the protocol's environment
    variables (`$GPORTAL_USERNAME` / `$GPORTAL_PASSWORD` and
    `$JAXA_PTREE_USERNAME` / `$JAXA_PTREE_PASSWORD` respectively) as
    fallback, and caches them on the instance for the branch to read
    through :attr:`username` / :attr:`password`. Idempotent — a
    second call after `is_authenticated()` returns `True`
    short-circuits.

    Raises:
        AuthenticationError: When the protocol is credentialed and
            neither the explicit credentials nor the environment
            variables supply a usable username + password pair. The
            message names the env vars and the free-registration URL
            for the protocol that failed to resolve.

    Examples:
        - The jaxa-earth protocol's configure is a no-op:
            ```python
            >>> from earthlens.jaxa import JaxaAuth, JaxaCredentials
            >>> auth = JaxaAuth(JaxaCredentials(), protocol="jaxa-earth")
            >>> auth.configure()
            >>> auth.is_authenticated()
            True

            ```
        - With gportal credentials supplied explicitly, configure caches them
          for the branch to read via `.username` / `.password`:
            ```python
            >>> from pydantic import SecretStr
            >>> from earthlens.jaxa import JaxaAuth, JaxaCredentials
            >>> creds = JaxaCredentials(
            ...     gportal_username="alice",
            ...     gportal_password=SecretStr("topsecret"),
            ... )
            >>> auth = JaxaAuth(creds, protocol="gportal")
            >>> auth.configure()
            >>> auth.username
            'alice'

            ```
        - The `ptree` protocol reads its own credential pair:
            ```python
            >>> from pydantic import SecretStr
            >>> from earthlens.jaxa import JaxaAuth, JaxaCredentials
            >>> creds = JaxaCredentials(
            ...     ptree_username="alice@example.org",
            ...     ptree_password=SecretStr("hunter2"),
            ... )
            >>> auth = JaxaAuth(creds, protocol="ptree")
            >>> auth.configure()
            >>> auth.username
            'alice@example.org'

            ```
    """
    if self._configured:
        return
    if self._protocol == "jaxa-earth":
        self._configured = True
        return
    spec = _PROTOCOL_SPECS[self._protocol]
    cred_username = getattr(self._creds, spec.cred_user)
    cred_password: SecretStr | None = getattr(self._creds, spec.cred_pass)
    username = cred_username or os.environ.get(spec.env_user)
    password_raw = (
        cred_password.get_secret_value()
        if cred_password is not None
        else os.environ.get(spec.env_pass)
    )
    if not username or not password_raw:
        raise AuthenticationError(
            f"no {spec.human_name} credentials available: pass "
            f"{spec.cred_user}= and {spec.cred_pass}= to JAXA(...), "
            f"or set both {spec.env_user} and {spec.env_pass} "
            f"environment variables. Register a free account at "
            f"{spec.register_url}."
        )
    self._username = username
    self._password = SecretStr(password_raw)
    self._configured = True

is_authenticated() #

Return True once :meth:configure has run successfully.

Returns:

Name Type Description
bool bool

True after :meth:configure has completed without raising; False before then.

Examples:

  • Construction does not authenticate; configure flips the flag:
    >>> from earthlens.jaxa import JaxaAuth, JaxaCredentials
    >>> auth = JaxaAuth(JaxaCredentials(), protocol="jaxa-earth")
    >>> auth.is_authenticated()
    False
    >>> auth.configure()
    >>> auth.is_authenticated()
    True
    
Source code in src/earthlens/jaxa/auth.py
def is_authenticated(self) -> bool:
    """Return `True` once :meth:`configure` has run successfully.

    Returns:
        bool: `True` after :meth:`configure` has completed without
            raising; `False` before then.

    Examples:
        - Construction does not authenticate; configure flips the flag:
            ```python
            >>> from earthlens.jaxa import JaxaAuth, JaxaCredentials
            >>> auth = JaxaAuth(JaxaCredentials(), protocol="jaxa-earth")
            >>> auth.is_authenticated()
            False
            >>> auth.configure()
            >>> auth.is_authenticated()
            True

            ```
    """
    return self._configured

JaxaCredentials #

Bases: BaseModel

Frozen value object holding the credentialed-branch secrets.

All fields are optional at construction time: None means "resolve from the corresponding environment variable at :meth:JaxaAuth.configure time". The real "are credentials available?" gate is :meth:JaxaAuth.configure, not this model.

G-Portal and P-Tree credentials are stored side-by-side because a single :class:JAXA instance is bound to one protocol per call — the unused pair is simply ignored.

Attributes:

Name Type Description
gportal_username str | None

G-Portal username. None defers to $GPORTAL_USERNAME at configure time.

gportal_password SecretStr | None

G-Portal password, stored as a SecretStr so it is never echoed in repr or logs. None defers to $GPORTAL_PASSWORD.

ptree_username str | None

P-Tree username (the registered email). None defers to $JAXA_PTREE_USERNAME.

ptree_password SecretStr | None

P-Tree password, stored as a SecretStr. None defers to $JAXA_PTREE_PASSWORD.

Examples:

  • The password is hidden in repr:
    >>> from earthlens.jaxa import JaxaCredentials
    >>> creds = JaxaCredentials(gportal_username="alice", gportal_password="topsecret")
    >>> "topsecret" in repr(creds)
    False
    >>> creds.gportal_password.get_secret_value()
    'topsecret'
    
  • The same protection applies to the P-Tree password:
    >>> from earthlens.jaxa import JaxaCredentials
    >>> creds = JaxaCredentials(
    ...     ptree_username="alice@example.org",
    ...     ptree_password="hunter2",
    ... )
    >>> "hunter2" in repr(creds)
    False
    
  • Every field is optional — rely on the environment instead:
    >>> from earthlens.jaxa import JaxaCredentials
    >>> JaxaCredentials().ptree_username is None
    True
    
Source code in src/earthlens/jaxa/auth.py
class JaxaCredentials(BaseModel):
    """Frozen value object holding the credentialed-branch secrets.

    All fields are optional at construction time: `None` means "resolve
    from the corresponding environment variable at :meth:`JaxaAuth.configure`
    time". The real "are credentials available?" gate is
    :meth:`JaxaAuth.configure`, not this model.

    G-Portal and P-Tree credentials are stored side-by-side because a
    single :class:`JAXA` instance is bound to one protocol per call — the
    unused pair is simply ignored.

    Attributes:
        gportal_username: G-Portal username. `None` defers to
            `$GPORTAL_USERNAME` at configure time.
        gportal_password: G-Portal password, stored as a `SecretStr` so it
            is never echoed in `repr` or logs. `None` defers to
            `$GPORTAL_PASSWORD`.
        ptree_username: P-Tree username (the registered email). `None`
            defers to `$JAXA_PTREE_USERNAME`.
        ptree_password: P-Tree password, stored as a `SecretStr`. `None`
            defers to `$JAXA_PTREE_PASSWORD`.

    Examples:
        - The password is hidden in `repr`:
            ```python
            >>> from earthlens.jaxa import JaxaCredentials
            >>> creds = JaxaCredentials(gportal_username="alice", gportal_password="topsecret")
            >>> "topsecret" in repr(creds)
            False
            >>> creds.gportal_password.get_secret_value()
            'topsecret'

            ```
        - The same protection applies to the P-Tree password:
            ```python
            >>> from earthlens.jaxa import JaxaCredentials
            >>> creds = JaxaCredentials(
            ...     ptree_username="alice@example.org",
            ...     ptree_password="hunter2",
            ... )
            >>> "hunter2" in repr(creds)
            False

            ```
        - Every field is optional — rely on the environment instead:
            ```python
            >>> from earthlens.jaxa import JaxaCredentials
            >>> JaxaCredentials().ptree_username is None
            True

            ```
    """

    model_config = ConfigDict(frozen=True)

    gportal_username: str | None = None
    gportal_password: SecretStr | None = None
    ptree_username: str | None = None
    ptree_password: SecretStr | None = None

RetentionError #

Bases: ValueError

Raised when a P-Tree request falls outside the 30-day archive.

P-Tree keeps only the last 30 days of HSD granules; older dates return an FTP 450 No such file or directory (not a permanent 550), which is opaque. This client-side guard fails fast with the exact archive window instead so users can adjust the request.

Source code in src/earthlens/jaxa/_ptree.py
class RetentionError(ValueError):
    """Raised when a P-Tree request falls outside the 30-day archive.

    P-Tree keeps only the last 30 days of HSD granules; older dates return
    an FTP `450 No such file or directory` (not a permanent `550`), which
    is opaque. This client-side guard fails fast with the exact archive
    window instead so users can adjust the request.
    """

clear_catalog_cache() #

Empty the module-level JAXA catalog parse cache.

Examples:

  • Calling it is a no-op when the cache is already empty, and subsequent Catalog() calls re-read the YAML from disk:
    >>> from earthlens.jaxa.catalog import clear_catalog_cache, Catalog
    >>> clear_catalog_cache()
    >>> "aw3d30" in Catalog()
    True
    
Source code in src/earthlens/jaxa/catalog.py
def clear_catalog_cache() -> None:
    """Empty the module-level JAXA catalog parse cache.

    Examples:
        - Calling it is a no-op when the cache is already empty, and
          subsequent `Catalog()` calls re-read the YAML from disk:
            ```python
            >>> from earthlens.jaxa.catalog import clear_catalog_cache, Catalog
            >>> clear_catalog_cache()
            >>> "aw3d30" in Catalog()
            True

            ```
    """
    _CATALOG_CACHE.clear()

earthlens.jaxa.backend #

JAXA backend — dispatches a request to one of three protocols.

JAXA reaches JAXA's Earth-observation archive via three complementary SDKs, selected per-dataset by the catalog's protocol discriminator:

  • protocol: jaxa-earth — authless STAC/COG access through the official jaxa.earth API. The API returns in-memory numpy arrays which the backend writes to north-up GeoTIFFs via pyramids.dataset.Dataset.
  • protocol: gportal — credentialed SFTP access through the community gportal SDK. The backend authenticates, searches, and downloads matching products into the output directory.
  • protocol: ptree — credentialed FTP access to ftp.ptree.jaxa.jp's Himawari-8/9 HSD granules (30-day rolling archive, 10 segments per band per 10-minute slot). Ships raw .DAT.bz2 files; decode is pyramids PY-2 (satpy reader bridge), not this backend.

Each download() call routes every requested key to its protocol branch (_jaxa_earth.fetch_jaxa_earth, _gportal.fetch_gportal, or _ptree.fetch_ptree). The three branches share the request shape (bbox + dates + a list of dataset keys); the catalog row carries the protocol-specific identifier, default band, and aliases. Mixing keys from more than one protocol in one call is rejected — the three paths emit different file types (GeoTIFFs vs raw SFTP products vs raw HSD granules) and have different concurrency profiles, so the API forces one protocol per call.

OUTPUT_KIND = "raster"; download() returns the list of written paths. The aggregate= argument raises NotImplementedError — multi-date windowed reductions are a follow-on enhancement once the array-stack shape stabilises across the catalog (see G6 in the planning doc).

JAXA #

Bases: AbstractDataSource

Unified JAXA backend over three protocols (jaxa-earth, gportal, ptree).

Attributes:

Name Type Description
OUTPUT_KIND OutputKind

Fixed "raster". The jaxa-earth branch always emits GeoTIFFs; the gportal branch writes raw products (HDF5, GeoTIFF, NetCDF — mission-dependent); the ptree branch writes raw Himawari HSD .DAT.bz2 segments (10 per band per 10-minute slot) — all three downstream readers still treat the output as gridded artefacts.

Source code in src/earthlens/jaxa/backend.py
class JAXA(AbstractDataSource):
    """Unified JAXA backend over three protocols (`jaxa-earth`, `gportal`, `ptree`).

    Attributes:
        OUTPUT_KIND: Fixed `"raster"`. The `jaxa-earth` branch always emits
            GeoTIFFs; the `gportal` branch writes raw products (HDF5,
            GeoTIFF, NetCDF — mission-dependent); the `ptree` branch
            writes raw Himawari HSD `.DAT.bz2` segments (10 per band per
            10-minute slot) — all three downstream readers still treat
            the output as gridded artefacts.
    """

    OUTPUT_KIND: OutputKind = "raster"

    def __init__(
        self,
        start: str,
        end: str,
        variables: list[str],
        lat_lim: list[float],
        lon_lim: list[float],
        temporal_resolution: str = "daily",
        path: Path | str = "",
        fmt: str = "%Y-%m-%d",
        *,
        resolution: float | None = None,
        bands: list[str] | None = None,
        gportal_username: str | None = None,
        gportal_password: str | None = None,
        ptree_username: str | None = None,
        ptree_password: str | None = None,
        catalog: Catalog | None = None,
    ):
        """Initialise a JAXA backend instance.

        Resolves every key against the catalog up front so that an unknown
        key (or a request that mixes more than one of the three protocols)
        fails at construction rather than mid-download.

        Args:
            start: Inclusive start of the date window (parsed with `fmt`).
            end: Inclusive end of the date window.
            variables: Catalog dataset keys, canonical or alias. Every key
                must resolve to the **same** protocol — mixing keys from
                more than one of `jaxa-earth`, `gportal`, and `ptree`
                raises with a diagnostic message that names each violated
                protocol and its offending keys.
            lat_lim: `[lat_min, lat_max]` in degrees.
            lon_lim: `[lon_min, lon_max]` in degrees.
            temporal_resolution: Advisory label only — JAXA Earth uses
                the full date range, G-Portal queries on
                `(start, end)`, and `_ptree.fetch_ptree` iterates every
                10-minute slot in the window.
            path: Output directory (created if missing).
            fmt: `strptime` format for `start` / `end`.
            resolution: `ppu` (pixels per degree) for the `jaxa-earth`
                branch. `None` lets the API pick its native resolution.
                Ignored for `gportal` and `ptree`.
            bands: Override the catalog's `default_band`. For
                `jaxa-earth`, picks the band(s) whose GeoTIFF the
                backend writes. For `ptree`, selects which Himawari
                AHI band(s) to download from the full 10-segment set
                — `"B01"`..`"B16"` are valid, with `"B03"` (0.5 km
                visible) and `"B13"` (2 km IR) as the two common
                defaults. `None` uses the row's `default_band`.
                Ignored for `gportal` (products are downloaded whole).
            gportal_username: Explicit G-Portal username. When omitted,
                `JaxaAuth.configure()` reads `$GPORTAL_USERNAME` at
                authentication time. Only used by the `gportal` branch.
            gportal_password: Explicit G-Portal password. When omitted,
                falls back to `$GPORTAL_PASSWORD`. Only used by the
                `gportal` branch.
            ptree_username: Explicit P-Tree username (the email
                registered at eorc.jaxa.jp/ptree). When omitted, falls
                back to `$JAXA_PTREE_USERNAME`. Only used by the
                `ptree` branch. Never reuse the G-Portal credentials —
                the two accounts are distinct.
            ptree_password: Explicit P-Tree password. When omitted,
                falls back to `$JAXA_PTREE_PASSWORD`. Only used by the
                `ptree` branch.
            catalog: Optional pre-built `Catalog` (tests inject a faked
                one); defaults to the bundled catalog.

        Raises:
            ValueError: If `variables` is empty, an alias is unknown, or
                the resolved keys span more than one protocol.
        """
        if not variables:
            raise ValueError(
                "JAXA requires a non-empty `variables` list of dataset keys, "
                'e.g. ["aw3d30"] or ["elevation"].'
            )

        self._catalog = catalog if catalog is not None else Catalog()
        self._resolution = resolution
        self._bands_override = bands

        resolved: list[Dataset] = [self._catalog.get(v) for v in variables]
        protocols = {ds.protocol for ds in resolved}
        if len(protocols) > 1:
            groups = {
                p: [ds.key for ds in resolved if ds.protocol == p]
                for p in sorted(protocols)
            }
            summary = ", ".join(f"{p}={ks}" for p, ks in groups.items())
            raise ValueError(
                "one JAXA request must target a single protocol, but the "
                f"requested variables span multiple: {summary}. "
                "Issue one JAXA(...) call per protocol."
            )
        self._resolved: list[Dataset] = resolved
        self._protocol: JaxaProtocol = next(iter(protocols))

        creds = JaxaCredentials(
            gportal_username=gportal_username,
            gportal_password=SecretStr(gportal_password)
            if gportal_password is not None
            else None,
            ptree_username=ptree_username,
            ptree_password=SecretStr(ptree_password)
            if ptree_password is not None
            else None,
        )
        # Bind the protocol so `AbstractDataSource.authenticate()` (which
        # calls `self._auth.configure()` with no args) fails fast on
        # missing credentials for whichever credentialed branch this
        # request pins (`gportal` or `ptree`); jaxa-earth is authless.
        self._auth = JaxaAuth(creds, protocol=self._protocol)

        super().__init__(
            start=start,
            end=end,
            variables=variables,
            temporal_resolution=temporal_resolution,
            lat_lim=lat_lim,
            lon_lim=lon_lim,
            fmt=fmt,
            path=path,
        )

    @property
    def auth(self) -> JaxaAuth:
        """The :class:`JaxaAuth` bound to this request.

        Returns:
            JaxaAuth: The optional-credentials auth object. `configure()`
            is a no-op for the `jaxa-earth` protocol, resolves
            G-Portal credentials for `gportal`, and resolves P-Tree
            credentials for `ptree`.

        Examples:
            - The auth object's protocol always matches the backend's:
                ```python
                >>> import tempfile
                >>> from earthlens.jaxa import JAXA
                >>> backend = JAXA(
                ...     start="2020-01-01", end="2020-12-31",
                ...     variables=["aw3d30"],
                ...     lat_lim=[35.0, 36.0], lon_lim=[138.0, 139.0],
                ...     path=tempfile.gettempdir(),
                ... )
                >>> backend.auth.protocol
                'jaxa-earth'

                ```
        """
        return self._auth

    @property
    def protocol(self) -> JaxaProtocol:
        """The single protocol this request resolved to.

        Returns:
            JaxaProtocol: One of `"jaxa-earth"`, `"gportal"`, or
                `"ptree"`.

        Examples:
            - Resolving an alias pins the protocol on construction:
                ```python
                >>> import tempfile
                >>> from earthlens.jaxa import JAXA
                >>> JAXA(
                ...     start="2020-01-01", end="2020-12-31",
                ...     variables=["elevation"],
                ...     lat_lim=[35.0, 36.0], lon_lim=[138.0, 139.0],
                ...     path=tempfile.gettempdir(),
                ... ).protocol
                'jaxa-earth'

                ```
        """
        return self._protocol

    def _initialize(self) -> None:
        """No eager connection setup.

        All three branches import their transport clients lazily:
        `jaxa.earth` and `gportal` inside their branch modules (needing
        the `[jaxa]` extra), and the `ptree` branch uses only stdlib
        `ftplib` on demand. `jaxa-earth` is authless; the optional
        `gportal` / `ptree` credentials are resolved lazily —
        `download()` calls `self._auth.configure()` before dispatching,
        and so does the explicit `EarthLens(...).authenticate()`
        entry point.

        Returns:
            None: No backend client to attach.
        """
        return None

    def _create_grid(self, lat_lim: list, lon_lim: list) -> SpatialExtent:
        """Wrap the user bbox into a `SpatialExtent`.

        Args:
            lat_lim: `[lat_min, lat_max]` in degrees.
            lon_lim: `[lon_min, lon_max]` in degrees.

        Returns:
            SpatialExtent: Validated, frozen bbox (WGS84).
        """
        return SpatialExtent.from_pairs(lat_lim=lat_lim, lon_lim=lon_lim)

    def _check_input_dates(
        self,
        start: str,
        end: str,
        temporal_resolution: str,
        fmt: str,
    ) -> TemporalExtent:
        """Parse the date window into a `TemporalExtent`.

        The frequency is advisory: `jaxa-earth` consumes a `dlim` range
        directly, `gportal.search` consumes `start_time` / `end_time`
        ISO strings, and `_ptree.fetch_ptree` iterates every 10-minute
        slot in the window.

        Args:
            start: Inclusive start of the window.
            end: Inclusive end of the window.
            temporal_resolution: Advisory label; defaults to `"daily"`.
            fmt: `strptime` format applied to `start` / `end`.

        Returns:
            TemporalExtent: Frozen model with the parsed bounds.

        Raises:
            ValueError: If `start` parses to a date later than `end`.
        """
        start_dt = dt.datetime.strptime(start, fmt)
        end_dt = dt.datetime.strptime(end, fmt)
        freq_alias = _FREQ_ALIAS.get(temporal_resolution, "MS")
        dates = pd.date_range(start_dt, end_dt, freq=freq_alias)
        return TemporalExtent(
            start_date=start_dt,
            end_date=end_dt,
            resolution=freq_alias,
            dates=dates,
        )

    def _api(self) -> list[Path]:
        """Dispatch to the matching protocol branch.

        Returns:
            list[Path]: One or more written paths per resolved dataset
                — a COG for `jaxa-earth`, an HDF5 / GeoTIFF / NetCDF
                product for `gportal` (depending on the mission), or
                the 10 raw `.DAT.bz2` HSD segments per band per
                10-minute slot for `ptree`.
        """
        out_dir = Path(self.path)
        self._auth.configure()
        written: list[Path] = []
        if self._protocol == "jaxa-earth":
            from earthlens.jaxa._jaxa_earth import fetch_jaxa_earth

            for ds in self._resolved:
                written.extend(
                    fetch_jaxa_earth(
                        dataset=ds,
                        space=self.space,
                        time=self.time,
                        resolution=self._resolution,
                        bands=self._bands_override,
                        out_dir=out_dir,
                    )
                )
        elif self._protocol == "gportal":
            from earthlens.jaxa._gportal import fetch_gportal

            for ds in self._resolved:
                written.extend(
                    fetch_gportal(
                        dataset=ds,
                        space=self.space,
                        time=self.time,
                        auth=self._auth,
                        out_dir=out_dir,
                    )
                )
        elif self._protocol == "ptree":
            from earthlens.jaxa._ptree import fetch_ptree

            for ds in self._resolved:
                written.extend(
                    fetch_ptree(
                        dataset=ds,
                        space=self.space,
                        time=self.time,
                        auth=self._auth,
                        out_dir=out_dir,
                        bands=self._bands_override,
                    )
                )
        else:
            # Defensive: `JaxaProtocol` is a `Literal[...]` and the
            # constructor rejects anything else, so this only fires if a
            # future fourth protocol is added to the literal but not to
            # `_api`. Failing loud beats silently routing to the last
            # `else` branch (Round 2 L3).
            raise ValueError(
                f"unhandled JAXA protocol {self._protocol!r}; add a "
                "dispatch branch alongside jaxa-earth/gportal/ptree."
            )
        return written

    def download(
        self,
        progress_bar: bool = True,
        aggregate: AggregationConfig | None = None,
    ) -> list[Path]:
        """Fetch the requested datasets and return the written paths.

        Args:
            progress_bar: Reserved for future per-download progress;
                each branch prints its own progress lines today
                (jaxa-earth via `jaxa.earth`, gportal via the `gportal`
                SDK, ptree via one segment-path write per FTP transfer).
            aggregate: Not supported — JAXA requests return per-date
                rasters; reducing them across dates is a follow-on.

        Returns:
            list[Path]: One or more files per resolved dataset, in
                request order.

        Raises:
            NotImplementedError: When `aggregate` is non-`None`. See `G6`
                in the planning doc.

        Examples:
            - Passing `aggregate=` raises because per-date stacks are not
              reduced yet:
                ```python
                >>> import tempfile
                >>> from earthlens.jaxa import JAXA
                >>> backend = JAXA(
                ...     start="2020-01-01", end="2020-12-31",
                ...     variables=["aw3d30"],
                ...     lat_lim=[35.0, 36.0], lon_lim=[138.0, 139.0],
                ...     path=tempfile.gettempdir(),
                ... )
                >>> backend.download(aggregate=object())  # doctest: +ELLIPSIS
                Traceback (most recent call last):
                    ...
                NotImplementedError: JAXA does not yet support the aggregate=...

                ```
        """
        if aggregate is not None:
            raise NotImplementedError(
                "JAXA does not yet support the aggregate= argument. "
                "All three branches emit per-date artefacts today "
                "(jaxa-earth: per-date COGs; gportal: per-product "
                "SFTP downloads; ptree: per-slot HSD segments) — "
                "reducing them across dates is a planned follow-on "
                "(planning G6)."
            )
        del progress_bar
        return self._api()

auth property #

The :class:JaxaAuth bound to this request.

Returns:

Name Type Description
JaxaAuth JaxaAuth

The optional-credentials auth object. configure()

JaxaAuth

is a no-op for the jaxa-earth protocol, resolves

JaxaAuth

G-Portal credentials for gportal, and resolves P-Tree

JaxaAuth

credentials for ptree.

Examples:

  • The auth object's protocol always matches the backend's:
    >>> import tempfile
    >>> from earthlens.jaxa import JAXA
    >>> backend = JAXA(
    ...     start="2020-01-01", end="2020-12-31",
    ...     variables=["aw3d30"],
    ...     lat_lim=[35.0, 36.0], lon_lim=[138.0, 139.0],
    ...     path=tempfile.gettempdir(),
    ... )
    >>> backend.auth.protocol
    'jaxa-earth'
    

protocol property #

The single protocol this request resolved to.

Returns:

Name Type Description
JaxaProtocol JaxaProtocol

One of "jaxa-earth", "gportal", or "ptree".

Examples:

  • Resolving an alias pins the protocol on construction:
    >>> import tempfile
    >>> from earthlens.jaxa import JAXA
    >>> JAXA(
    ...     start="2020-01-01", end="2020-12-31",
    ...     variables=["elevation"],
    ...     lat_lim=[35.0, 36.0], lon_lim=[138.0, 139.0],
    ...     path=tempfile.gettempdir(),
    ... ).protocol
    'jaxa-earth'
    

__init__(start, end, variables, lat_lim, lon_lim, temporal_resolution='daily', path='', fmt='%Y-%m-%d', *, resolution=None, bands=None, gportal_username=None, gportal_password=None, ptree_username=None, ptree_password=None, catalog=None) #

Initialise a JAXA backend instance.

Resolves every key against the catalog up front so that an unknown key (or a request that mixes more than one of the three protocols) fails at construction rather than mid-download.

Parameters:

Name Type Description Default
start str

Inclusive start of the date window (parsed with fmt).

required
end str

Inclusive end of the date window.

required
variables list[str]

Catalog dataset keys, canonical or alias. Every key must resolve to the same protocol — mixing keys from more than one of jaxa-earth, gportal, and ptree raises with a diagnostic message that names each violated protocol and its offending keys.

required
lat_lim list[float]

[lat_min, lat_max] in degrees.

required
lon_lim list[float]

[lon_min, lon_max] in degrees.

required
temporal_resolution str

Advisory label only — JAXA Earth uses the full date range, G-Portal queries on (start, end), and _ptree.fetch_ptree iterates every 10-minute slot in the window.

'daily'
path Path | str

Output directory (created if missing).

''
fmt str

strptime format for start / end.

'%Y-%m-%d'
resolution float | None

ppu (pixels per degree) for the jaxa-earth branch. None lets the API pick its native resolution. Ignored for gportal and ptree.

None
bands list[str] | None

Override the catalog's default_band. For jaxa-earth, picks the band(s) whose GeoTIFF the backend writes. For ptree, selects which Himawari AHI band(s) to download from the full 10-segment set — "B01".."B16" are valid, with "B03" (0.5 km visible) and "B13" (2 km IR) as the two common defaults. None uses the row's default_band. Ignored for gportal (products are downloaded whole).

None
gportal_username str | None

Explicit G-Portal username. When omitted, JaxaAuth.configure() reads $GPORTAL_USERNAME at authentication time. Only used by the gportal branch.

None
gportal_password str | None

Explicit G-Portal password. When omitted, falls back to $GPORTAL_PASSWORD. Only used by the gportal branch.

None
ptree_username str | None

Explicit P-Tree username (the email registered at eorc.jaxa.jp/ptree). When omitted, falls back to $JAXA_PTREE_USERNAME. Only used by the ptree branch. Never reuse the G-Portal credentials — the two accounts are distinct.

None
ptree_password str | None

Explicit P-Tree password. When omitted, falls back to $JAXA_PTREE_PASSWORD. Only used by the ptree branch.

None
catalog Catalog | None

Optional pre-built Catalog (tests inject a faked one); defaults to the bundled catalog.

None

Raises:

Type Description
ValueError

If variables is empty, an alias is unknown, or the resolved keys span more than one protocol.

Source code in src/earthlens/jaxa/backend.py
def __init__(
    self,
    start: str,
    end: str,
    variables: list[str],
    lat_lim: list[float],
    lon_lim: list[float],
    temporal_resolution: str = "daily",
    path: Path | str = "",
    fmt: str = "%Y-%m-%d",
    *,
    resolution: float | None = None,
    bands: list[str] | None = None,
    gportal_username: str | None = None,
    gportal_password: str | None = None,
    ptree_username: str | None = None,
    ptree_password: str | None = None,
    catalog: Catalog | None = None,
):
    """Initialise a JAXA backend instance.

    Resolves every key against the catalog up front so that an unknown
    key (or a request that mixes more than one of the three protocols)
    fails at construction rather than mid-download.

    Args:
        start: Inclusive start of the date window (parsed with `fmt`).
        end: Inclusive end of the date window.
        variables: Catalog dataset keys, canonical or alias. Every key
            must resolve to the **same** protocol — mixing keys from
            more than one of `jaxa-earth`, `gportal`, and `ptree`
            raises with a diagnostic message that names each violated
            protocol and its offending keys.
        lat_lim: `[lat_min, lat_max]` in degrees.
        lon_lim: `[lon_min, lon_max]` in degrees.
        temporal_resolution: Advisory label only — JAXA Earth uses
            the full date range, G-Portal queries on
            `(start, end)`, and `_ptree.fetch_ptree` iterates every
            10-minute slot in the window.
        path: Output directory (created if missing).
        fmt: `strptime` format for `start` / `end`.
        resolution: `ppu` (pixels per degree) for the `jaxa-earth`
            branch. `None` lets the API pick its native resolution.
            Ignored for `gportal` and `ptree`.
        bands: Override the catalog's `default_band`. For
            `jaxa-earth`, picks the band(s) whose GeoTIFF the
            backend writes. For `ptree`, selects which Himawari
            AHI band(s) to download from the full 10-segment set
            — `"B01"`..`"B16"` are valid, with `"B03"` (0.5 km
            visible) and `"B13"` (2 km IR) as the two common
            defaults. `None` uses the row's `default_band`.
            Ignored for `gportal` (products are downloaded whole).
        gportal_username: Explicit G-Portal username. When omitted,
            `JaxaAuth.configure()` reads `$GPORTAL_USERNAME` at
            authentication time. Only used by the `gportal` branch.
        gportal_password: Explicit G-Portal password. When omitted,
            falls back to `$GPORTAL_PASSWORD`. Only used by the
            `gportal` branch.
        ptree_username: Explicit P-Tree username (the email
            registered at eorc.jaxa.jp/ptree). When omitted, falls
            back to `$JAXA_PTREE_USERNAME`. Only used by the
            `ptree` branch. Never reuse the G-Portal credentials —
            the two accounts are distinct.
        ptree_password: Explicit P-Tree password. When omitted,
            falls back to `$JAXA_PTREE_PASSWORD`. Only used by the
            `ptree` branch.
        catalog: Optional pre-built `Catalog` (tests inject a faked
            one); defaults to the bundled catalog.

    Raises:
        ValueError: If `variables` is empty, an alias is unknown, or
            the resolved keys span more than one protocol.
    """
    if not variables:
        raise ValueError(
            "JAXA requires a non-empty `variables` list of dataset keys, "
            'e.g. ["aw3d30"] or ["elevation"].'
        )

    self._catalog = catalog if catalog is not None else Catalog()
    self._resolution = resolution
    self._bands_override = bands

    resolved: list[Dataset] = [self._catalog.get(v) for v in variables]
    protocols = {ds.protocol for ds in resolved}
    if len(protocols) > 1:
        groups = {
            p: [ds.key for ds in resolved if ds.protocol == p]
            for p in sorted(protocols)
        }
        summary = ", ".join(f"{p}={ks}" for p, ks in groups.items())
        raise ValueError(
            "one JAXA request must target a single protocol, but the "
            f"requested variables span multiple: {summary}. "
            "Issue one JAXA(...) call per protocol."
        )
    self._resolved: list[Dataset] = resolved
    self._protocol: JaxaProtocol = next(iter(protocols))

    creds = JaxaCredentials(
        gportal_username=gportal_username,
        gportal_password=SecretStr(gportal_password)
        if gportal_password is not None
        else None,
        ptree_username=ptree_username,
        ptree_password=SecretStr(ptree_password)
        if ptree_password is not None
        else None,
    )
    # Bind the protocol so `AbstractDataSource.authenticate()` (which
    # calls `self._auth.configure()` with no args) fails fast on
    # missing credentials for whichever credentialed branch this
    # request pins (`gportal` or `ptree`); jaxa-earth is authless.
    self._auth = JaxaAuth(creds, protocol=self._protocol)

    super().__init__(
        start=start,
        end=end,
        variables=variables,
        temporal_resolution=temporal_resolution,
        lat_lim=lat_lim,
        lon_lim=lon_lim,
        fmt=fmt,
        path=path,
    )

download(progress_bar=True, aggregate=None) #

Fetch the requested datasets and return the written paths.

Parameters:

Name Type Description Default
progress_bar bool

Reserved for future per-download progress; each branch prints its own progress lines today (jaxa-earth via jaxa.earth, gportal via the gportal SDK, ptree via one segment-path write per FTP transfer).

True
aggregate AggregationConfig | None

Not supported — JAXA requests return per-date rasters; reducing them across dates is a follow-on.

None

Returns:

Type Description
list[Path]

list[Path]: One or more files per resolved dataset, in request order.

Raises:

Type Description
NotImplementedError

When aggregate is non-None. See G6 in the planning doc.

Examples:

  • Passing aggregate= raises because per-date stacks are not reduced yet:
    >>> import tempfile
    >>> from earthlens.jaxa import JAXA
    >>> backend = JAXA(
    ...     start="2020-01-01", end="2020-12-31",
    ...     variables=["aw3d30"],
    ...     lat_lim=[35.0, 36.0], lon_lim=[138.0, 139.0],
    ...     path=tempfile.gettempdir(),
    ... )
    >>> backend.download(aggregate=object())  # doctest: +ELLIPSIS
    Traceback (most recent call last):
        ...
    NotImplementedError: JAXA does not yet support the aggregate=...
    
Source code in src/earthlens/jaxa/backend.py
def download(
    self,
    progress_bar: bool = True,
    aggregate: AggregationConfig | None = None,
) -> list[Path]:
    """Fetch the requested datasets and return the written paths.

    Args:
        progress_bar: Reserved for future per-download progress;
            each branch prints its own progress lines today
            (jaxa-earth via `jaxa.earth`, gportal via the `gportal`
            SDK, ptree via one segment-path write per FTP transfer).
        aggregate: Not supported — JAXA requests return per-date
            rasters; reducing them across dates is a follow-on.

    Returns:
        list[Path]: One or more files per resolved dataset, in
            request order.

    Raises:
        NotImplementedError: When `aggregate` is non-`None`. See `G6`
            in the planning doc.

    Examples:
        - Passing `aggregate=` raises because per-date stacks are not
          reduced yet:
            ```python
            >>> import tempfile
            >>> from earthlens.jaxa import JAXA
            >>> backend = JAXA(
            ...     start="2020-01-01", end="2020-12-31",
            ...     variables=["aw3d30"],
            ...     lat_lim=[35.0, 36.0], lon_lim=[138.0, 139.0],
            ...     path=tempfile.gettempdir(),
            ... )
            >>> backend.download(aggregate=object())  # doctest: +ELLIPSIS
            Traceback (most recent call last):
                ...
            NotImplementedError: JAXA does not yet support the aggregate=...

            ```
    """
    if aggregate is not None:
        raise NotImplementedError(
            "JAXA does not yet support the aggregate= argument. "
            "All three branches emit per-date artefacts today "
            "(jaxa-earth: per-date COGs; gportal: per-product "
            "SFTP downloads; ptree: per-slot HSD segments) — "
            "reducing them across dates is a planned follow-on "
            "(planning G6)."
        )
    del progress_bar
    return self._api()

earthlens.jaxa.catalog #

Dataset catalog for the JAXA backend.

Hosts :class:Catalog, the pydantic-backed reader for the bundled JAXA catalog. The catalog ships as a directory of per-mission YAML files at src/earthlens/jaxa/catalog/ (jaxa-earth.yaml, sgli.yaml, amsr.yaml, alos-palsar.yaml, earthcare.yaml, precipitation.yaml, …), plus a single _index.yaml carrying the merged available_datasets: list — the same sharded layout used by the gee and ecmwf siblings.

Each catalog row is a Dataset carrying a protocol discriminator ("jaxa-earth", "gportal", or "ptree") plus the protocol-specific identifier — a jaxa.earth STAC collection name for "jaxa-earth", a G-Portal numeric dataset id for "gportal", or a P-Tree product token (e.g. "himawari-ahi-fldk") for "ptree". The catalog's by_protocol(...) view + dataset-level validator together let the backend route a request to the right branch without re-parsing the YAML at every fetch.

The aliases map is a separate small index resolved by :meth:Catalog.resolve, giving every dataset a friendly key ("elevation""aw3d30") on top of its canonical key. The model is extra="forbid" so typos in the bundled YAML fail loudly on first load rather than silently being ignored.

CATALOG_PATH points at the bundled catalog directory (or, for tests that monkey-patch it, a single *.yaml file); :func:clear_catalog_cache empties the (path, mtime) parse cache used by :meth:Catalog.load.

Catalog #

Bases: AbstractCatalog

Reader for the bundled JAXA dataset catalog.

Subclasses :class:AbstractCatalog so the dict-like surface (cat["aw3d30"], "aw3d30" in cat, len(cat)) comes for free. The resolve method maps a friendly alias ("elevation") to its canonical key ("aw3d30"); :meth:by_protocol lists the keys filtered by protocol; :meth:get is a thin alias for get_dataset.

Attributes:

Name Type Description
datasets dict[str, Dataset]

Map from canonical key to its :class:Dataset row.

available_datasets list[str]

Sorted canonical keys.

Examples:

  • List the canonical keys, resolve a friendly alias:
    >>> from earthlens.jaxa import Catalog
    >>> cat = Catalog()
    >>> "aw3d30" in cat
    True
    >>> cat.resolve("elevation")
    'aw3d30'
    
Source code in src/earthlens/jaxa/catalog.py
class Catalog(AbstractCatalog):
    """Reader for the bundled JAXA dataset catalog.

    Subclasses :class:`AbstractCatalog` so the dict-like surface
    (`cat["aw3d30"]`, `"aw3d30" in cat`, `len(cat)`) comes for free. The
    `resolve` method maps a friendly alias (`"elevation"`) to its canonical
    key (`"aw3d30"`); :meth:`by_protocol` lists the keys filtered by
    protocol; :meth:`get` is a thin alias for `get_dataset`.

    Attributes:
        datasets: Map from canonical key to its :class:`Dataset` row.
        available_datasets: Sorted canonical keys.

    Examples:
        - List the canonical keys, resolve a friendly alias:
            ```python
            >>> from earthlens.jaxa import Catalog
            >>> cat = Catalog()
            >>> "aw3d30" in cat
            True
            >>> cat.resolve("elevation")
            'aw3d30'

            ```
    """

    _catalog_kind: str = "JAXA catalog"

    datasets: dict[str, Dataset] = Field(default_factory=dict)
    aliases: dict[str, str] = Field(default_factory=dict)

    def model_post_init(self, __context: Any) -> None:
        """Auto-load the bundled catalog when no datasets were supplied.

        `Catalog()` with no args reads :data:`CATALOG_PATH` (cached by
        `(path, mtime)`); passing `datasets=...` skips the disk read (used
        in tests). Either way the `aliases` map is derived from the loaded
        rows. `available_datasets` is populated from the YAML's optional
        `available_datasets:` block (the refresh CLI's `--write` target —
        an informational index of every live SDK id across all three
        protocols) or, when that block is absent, defaults to the sorted
        curated keys so the dict-like surface (`len(cat)` etc.) still
        works.
        """
        if not self.datasets:
            loaded = Catalog.load()
            self.datasets = loaded.datasets
            if loaded.available_datasets and not self.available_datasets:
                self.available_datasets = loaded.available_datasets
        # Build the alias index from the rows' `aliases` lists. Conflicts
        # (same alias on two rows) raise; canonical keys also resolve to
        # themselves so `resolve()` is total over both.
        aliases: dict[str, str] = {}
        for key, ds in self.datasets.items():
            aliases[key] = key
            for alias in ds.aliases:
                if alias in aliases and aliases[alias] != key:
                    raise ValueError(
                        f"alias {alias!r} is claimed by both {aliases[alias]!r} "
                        f"and {key!r} — pick one."
                    )
                aliases[alias] = key
        self.aliases = aliases
        if not self.available_datasets:
            self.available_datasets = sorted(self.datasets)

    @classmethod
    def load(cls, catalog_path: Path | None = None) -> Catalog:
        """Read and validate the JAXA catalog from disk (cached).

        Args:
            catalog_path: Path to the catalog directory (the sharded
                default) or a single YAML file (tests / legacy). Defaults
                to the module-level :data:`CATALOG_PATH`.

        Returns:
            A fully-populated :class:`Catalog` merged across every
            contributing file.

        Raises:
            ValueError: If the path has no `datasets:` rows across any
                file, or any row fails validation (missing identifier,
                alias conflict), or a key is declared in two shards.
            FileNotFoundError: If `catalog_path` does not exist (the
                bundled catalog ships with the wheel, so this only fires
                when a caller passes an explicit, missing path).
        """
        path = catalog_path if catalog_path is not None else CATALOG_PATH
        files = _yaml_files_for(path)
        try:
            mtime_tuple = tuple((str(f), f.stat().st_mtime_ns) for f in files)
        except FileNotFoundError:
            mtime_tuple = ((str(path.resolve()), 0),)
        cache_key = (str(path.resolve()), mtime_tuple)
        cached = _CATALOG_CACHE.get(cache_key)
        if cached is not None:
            return cached

        merged_rows: dict[str, dict[str, Any]] = {}
        row_origin: dict[str, Path] = {}
        available: list[str] = []
        for file_path in files:
            data = load_yaml_strict(file_path) or {}
            for ident in data.get("available_datasets") or []:
                available.append(str(ident))
            for key, body in (data.get("datasets") or {}).items():
                if key in merged_rows:
                    raise ValueError(
                        f"dataset {key!r} declared in two catalog files: "
                        f"{row_origin[key]} and {file_path}"
                    )
                merged_rows[key] = dict(body or {})
                row_origin[key] = file_path

        if not merged_rows:
            raise ValueError(
                f"{path} is missing or has an empty 'datasets:' block. "
                "The JAXA catalog must list at least one dataset."
            )
        datasets: dict[str, Dataset] = {}
        for key, body in merged_rows.items():
            try:
                datasets[key] = Dataset(key=key, **body)
            except ValidationError as exc:
                raise ValueError(
                    f"{row_origin[key]} dataset {key!r} failed validation:\n{exc}"
                ) from exc
        catalog = cls(datasets=datasets, available_datasets=available)
        _CATALOG_CACHE[cache_key] = catalog
        return catalog

    def get_catalog(self) -> dict[str, Dataset]:
        """Return the dataset map (satisfies the abstract contract).

        Returns:
            dict[str, Dataset]: Same object as :attr:`datasets`.

        Examples:
            - Inspect one of the rows by canonical key:
                ```python
                >>> from earthlens.jaxa import Catalog
                >>> cat = Catalog()
                >>> cat.get_catalog()["aw3d30"].protocol
                'jaxa-earth'

                ```
        """
        return self.datasets

    def get(self, key: str) -> Dataset:
        """Return the :class:`Dataset` for `key` (canonical, alias, or raw id).

        Resolves `key` through :meth:`resolve` first, then returns the
        matching curated row when one exists. **Raw G-Portal numeric ids**
        (e.g. ``"11001002"``) and **raw jaxa.earth STAC collection names**
        (e.g. ``"JAXA.G-Portal_GCOM-W.AMSR2_standard.L3-SSW.daytime.v4_global_yearly"``)
        pass through unmapped: a passthrough :class:`Dataset` row is
        synthesized on the fly with the right protocol so the backend
        dispatches correctly — the full 799-entry G-Portal universe + the
        ~119-entry JAXA Earth STAC catalogue are usable without bloating
        the bundled YAML with hand-curated rows.

        Args:
            key: A canonical key, friendly alias, raw G-Portal id, or raw
                jaxa.earth STAC collection name.

        Returns:
            Dataset: The matching curated row, or a synthesized passthrough
            row when `key` is a raw id recognised by the live SDK.

        Raises:
            ValueError: If `key` is neither curated nor a syntactically
                valid raw id.

        Examples:
            - A friendly alias resolves to the same row as the canonical key:
                ```python
                >>> from earthlens.jaxa import Catalog
                >>> cat = Catalog()
                >>> cat.get("elevation").collection
                'JAXA.EORC_ALOS.PRISM_AW3D30.v3.2_global'
                >>> cat.get("aw3d30").default_band
                'DSM'

                ```
            - A raw G-Portal id passes through as a synthesized gportal row:
                ```python
                >>> from earthlens.jaxa import Catalog
                >>> row = Catalog().get("11001002")
                >>> row.protocol, row.short_name
                ('gportal', '11001002')

                ```
            - An unknown key raises ValueError:
                ```python
                >>> from earthlens.jaxa import Catalog
                >>> try:
                ...     Catalog().get("definitely-not-a-key")
                ... except ValueError as exc:
                ...     "JAXA catalog" in str(exc)
                True

                ```
        """
        canonical = self.resolve(key)
        if canonical in self.datasets:
            return self.datasets[canonical]
        # Passthrough: synthesize a Dataset row for raw ids that match
        # either protocol's id shape (resolve() has already accepted them).
        if _GPORTAL_ID_RE.match(canonical):
            return Dataset(key=canonical, protocol="gportal", short_name=canonical)
        return Dataset(key=canonical, protocol="jaxa-earth", collection=canonical)

    def resolve(self, key: str) -> str:
        """Map a friendly alias / raw id to its canonical key.

        Args:
            key: A canonical key, friendly alias, raw G-Portal numeric
                id (e.g. ``"11001002"``), or raw jaxa.earth STAC
                collection name (e.g. ``"JAXA.EORC_ALOS.PRISM_AW3D30.v3.2_global"``).

        Returns:
            str: The canonical catalog key. Raw ids pass through unchanged.

        Raises:
            ValueError: When `key` is neither curated nor a syntactically
                valid raw id, with a did-you-mean hint drawn from the
                union of canonical keys and aliases.

        Examples:
            - A canonical key resolves to itself; an alias resolves to its row:
                ```python
                >>> from earthlens.jaxa import Catalog
                >>> cat = Catalog()
                >>> cat.resolve("aw3d30")
                'aw3d30'
                >>> cat.resolve("elevation")
                'aw3d30'

                ```
            - Raw G-Portal numeric ids pass through unmapped:
                ```python
                >>> from earthlens.jaxa import Catalog
                >>> Catalog().resolve("11001002")
                '11001002'

                ```
        """
        if key in self.aliases:
            return self.aliases[key]
        # Raw G-Portal id (7-9 digits) or raw jaxa.earth STAC collection name
        # passes through unmapped, mirroring `usgs_water.Catalog.resolve` for
        # raw 5-digit NWIS codes. The available_datasets: index lets a
        # curator verify the id is real upstream.
        if _GPORTAL_ID_RE.match(key) or _JAXA_EARTH_PREFIX_RE.match(key):
            return key
        close = difflib.get_close_matches(key, self.aliases, n=1)
        hint = f" Did you mean {close[0]!r}?" if close else ""
        raise ValueError(
            f"{key!r} is not in the JAXA catalog. "
            f"Known keys: {sorted(self.datasets)}.{hint}"
        )

    def by_protocol(self, protocol: JaxaProtocol) -> list[str]:
        """Return canonical keys whose `protocol` matches `protocol`.

        Args:
            protocol: One of `"jaxa-earth"`, `"gportal"`, or `"ptree"`.

        Returns:
            list[str]: Sorted canonical keys filtered by protocol.

        Examples:
            - The bundled catalog ships all three protocols; `aw3d30` is
              jaxa-earth, `sgli-l3-nwlr` is gportal, and
              `himawari-ahi-fldk` is ptree:
                ```python
                >>> from earthlens.jaxa import Catalog
                >>> cat = Catalog()
                >>> "aw3d30" in cat.by_protocol("jaxa-earth")
                True
                >>> "sgli-l3-nwlr" in cat.by_protocol("gportal")
                True
                >>> "himawari-ahi-fldk" in cat.by_protocol("ptree")
                True

                ```
        """
        return sorted(k for k, ds in self.datasets.items() if ds.protocol == protocol)

    def __contains__(self, name: object) -> bool:
        """`name in cat` — accept both canonical keys and friendly aliases.

        Overrides :meth:`AbstractCatalog.__contains__`, which only checks
        `self.datasets`, so the alias surface (`"elevation" in cat`)
        matches what `cat.get("elevation")` already accepts.

        Examples:
            - Aliases and canonical keys both resolve:
                ```python
                >>> from earthlens.jaxa import Catalog
                >>> cat = Catalog()
                >>> "aw3d30" in cat
                True
                >>> "elevation" in cat
                True
                >>> "not-a-real-key" in cat
                False

                ```
        """
        return name in self.aliases

    def __getitem__(self, name: str) -> Dataset:
        """`cat[name]` — accept both canonical keys and friendly aliases.

        Overrides :meth:`AbstractCatalog.__getitem__` so dict-style
        lookup matches the same alias surface as :meth:`get`. Raises
        :class:`KeyError` on miss (the dict-style contract; callers that
        want a did-you-mean hint use :meth:`get` instead).

        Examples:
            - Lookup by alias returns the canonical row:
                ```python
                >>> from earthlens.jaxa import Catalog
                >>> cat = Catalog()
                >>> cat["elevation"].key
                'aw3d30'

                ```
        """
        try:
            return self.get(name)
        except ValueError as exc:
            raise KeyError(name) from exc

__contains__(name) #

name in cat — accept both canonical keys and friendly aliases.

Overrides :meth:AbstractCatalog.__contains__, which only checks self.datasets, so the alias surface ("elevation" in cat) matches what cat.get("elevation") already accepts.

Examples:

  • Aliases and canonical keys both resolve:
    >>> from earthlens.jaxa import Catalog
    >>> cat = Catalog()
    >>> "aw3d30" in cat
    True
    >>> "elevation" in cat
    True
    >>> "not-a-real-key" in cat
    False
    
Source code in src/earthlens/jaxa/catalog.py
def __contains__(self, name: object) -> bool:
    """`name in cat` — accept both canonical keys and friendly aliases.

    Overrides :meth:`AbstractCatalog.__contains__`, which only checks
    `self.datasets`, so the alias surface (`"elevation" in cat`)
    matches what `cat.get("elevation")` already accepts.

    Examples:
        - Aliases and canonical keys both resolve:
            ```python
            >>> from earthlens.jaxa import Catalog
            >>> cat = Catalog()
            >>> "aw3d30" in cat
            True
            >>> "elevation" in cat
            True
            >>> "not-a-real-key" in cat
            False

            ```
    """
    return name in self.aliases

__getitem__(name) #

cat[name] — accept both canonical keys and friendly aliases.

Overrides :meth:AbstractCatalog.__getitem__ so dict-style lookup matches the same alias surface as :meth:get. Raises :class:KeyError on miss (the dict-style contract; callers that want a did-you-mean hint use :meth:get instead).

Examples:

  • Lookup by alias returns the canonical row:
    >>> from earthlens.jaxa import Catalog
    >>> cat = Catalog()
    >>> cat["elevation"].key
    'aw3d30'
    
Source code in src/earthlens/jaxa/catalog.py
def __getitem__(self, name: str) -> Dataset:
    """`cat[name]` — accept both canonical keys and friendly aliases.

    Overrides :meth:`AbstractCatalog.__getitem__` so dict-style
    lookup matches the same alias surface as :meth:`get`. Raises
    :class:`KeyError` on miss (the dict-style contract; callers that
    want a did-you-mean hint use :meth:`get` instead).

    Examples:
        - Lookup by alias returns the canonical row:
            ```python
            >>> from earthlens.jaxa import Catalog
            >>> cat = Catalog()
            >>> cat["elevation"].key
            'aw3d30'

            ```
    """
    try:
        return self.get(name)
    except ValueError as exc:
        raise KeyError(name) from exc

by_protocol(protocol) #

Return canonical keys whose protocol matches protocol.

Parameters:

Name Type Description Default
protocol JaxaProtocol

One of "jaxa-earth", "gportal", or "ptree".

required

Returns:

Type Description
list[str]

list[str]: Sorted canonical keys filtered by protocol.

Examples:

  • The bundled catalog ships all three protocols; aw3d30 is jaxa-earth, sgli-l3-nwlr is gportal, and himawari-ahi-fldk is ptree:
    >>> from earthlens.jaxa import Catalog
    >>> cat = Catalog()
    >>> "aw3d30" in cat.by_protocol("jaxa-earth")
    True
    >>> "sgli-l3-nwlr" in cat.by_protocol("gportal")
    True
    >>> "himawari-ahi-fldk" in cat.by_protocol("ptree")
    True
    
Source code in src/earthlens/jaxa/catalog.py
def by_protocol(self, protocol: JaxaProtocol) -> list[str]:
    """Return canonical keys whose `protocol` matches `protocol`.

    Args:
        protocol: One of `"jaxa-earth"`, `"gportal"`, or `"ptree"`.

    Returns:
        list[str]: Sorted canonical keys filtered by protocol.

    Examples:
        - The bundled catalog ships all three protocols; `aw3d30` is
          jaxa-earth, `sgli-l3-nwlr` is gportal, and
          `himawari-ahi-fldk` is ptree:
            ```python
            >>> from earthlens.jaxa import Catalog
            >>> cat = Catalog()
            >>> "aw3d30" in cat.by_protocol("jaxa-earth")
            True
            >>> "sgli-l3-nwlr" in cat.by_protocol("gportal")
            True
            >>> "himawari-ahi-fldk" in cat.by_protocol("ptree")
            True

            ```
    """
    return sorted(k for k, ds in self.datasets.items() if ds.protocol == protocol)

get(key) #

Return the :class:Dataset for key (canonical, alias, or raw id).

Resolves key through :meth:resolve first, then returns the matching curated row when one exists. Raw G-Portal numeric ids (e.g. "11001002") and raw jaxa.earth STAC collection names (e.g. "JAXA.G-Portal_GCOM-W.AMSR2_standard.L3-SSW.daytime.v4_global_yearly") pass through unmapped: a passthrough :class:Dataset row is synthesized on the fly with the right protocol so the backend dispatches correctly — the full 799-entry G-Portal universe + the ~119-entry JAXA Earth STAC catalogue are usable without bloating the bundled YAML with hand-curated rows.

Parameters:

Name Type Description Default
key str

A canonical key, friendly alias, raw G-Portal id, or raw jaxa.earth STAC collection name.

required

Returns:

Name Type Description
Dataset Dataset

The matching curated row, or a synthesized passthrough

Dataset

row when key is a raw id recognised by the live SDK.

Raises:

Type Description
ValueError

If key is neither curated nor a syntactically valid raw id.

Examples:

  • A friendly alias resolves to the same row as the canonical key:
    >>> from earthlens.jaxa import Catalog
    >>> cat = Catalog()
    >>> cat.get("elevation").collection
    'JAXA.EORC_ALOS.PRISM_AW3D30.v3.2_global'
    >>> cat.get("aw3d30").default_band
    'DSM'
    
  • A raw G-Portal id passes through as a synthesized gportal row:
    >>> from earthlens.jaxa import Catalog
    >>> row = Catalog().get("11001002")
    >>> row.protocol, row.short_name
    ('gportal', '11001002')
    
  • An unknown key raises ValueError:
    >>> from earthlens.jaxa import Catalog
    >>> try:
    ...     Catalog().get("definitely-not-a-key")
    ... except ValueError as exc:
    ...     "JAXA catalog" in str(exc)
    True
    
Source code in src/earthlens/jaxa/catalog.py
def get(self, key: str) -> Dataset:
    """Return the :class:`Dataset` for `key` (canonical, alias, or raw id).

    Resolves `key` through :meth:`resolve` first, then returns the
    matching curated row when one exists. **Raw G-Portal numeric ids**
    (e.g. ``"11001002"``) and **raw jaxa.earth STAC collection names**
    (e.g. ``"JAXA.G-Portal_GCOM-W.AMSR2_standard.L3-SSW.daytime.v4_global_yearly"``)
    pass through unmapped: a passthrough :class:`Dataset` row is
    synthesized on the fly with the right protocol so the backend
    dispatches correctly — the full 799-entry G-Portal universe + the
    ~119-entry JAXA Earth STAC catalogue are usable without bloating
    the bundled YAML with hand-curated rows.

    Args:
        key: A canonical key, friendly alias, raw G-Portal id, or raw
            jaxa.earth STAC collection name.

    Returns:
        Dataset: The matching curated row, or a synthesized passthrough
        row when `key` is a raw id recognised by the live SDK.

    Raises:
        ValueError: If `key` is neither curated nor a syntactically
            valid raw id.

    Examples:
        - A friendly alias resolves to the same row as the canonical key:
            ```python
            >>> from earthlens.jaxa import Catalog
            >>> cat = Catalog()
            >>> cat.get("elevation").collection
            'JAXA.EORC_ALOS.PRISM_AW3D30.v3.2_global'
            >>> cat.get("aw3d30").default_band
            'DSM'

            ```
        - A raw G-Portal id passes through as a synthesized gportal row:
            ```python
            >>> from earthlens.jaxa import Catalog
            >>> row = Catalog().get("11001002")
            >>> row.protocol, row.short_name
            ('gportal', '11001002')

            ```
        - An unknown key raises ValueError:
            ```python
            >>> from earthlens.jaxa import Catalog
            >>> try:
            ...     Catalog().get("definitely-not-a-key")
            ... except ValueError as exc:
            ...     "JAXA catalog" in str(exc)
            True

            ```
    """
    canonical = self.resolve(key)
    if canonical in self.datasets:
        return self.datasets[canonical]
    # Passthrough: synthesize a Dataset row for raw ids that match
    # either protocol's id shape (resolve() has already accepted them).
    if _GPORTAL_ID_RE.match(canonical):
        return Dataset(key=canonical, protocol="gportal", short_name=canonical)
    return Dataset(key=canonical, protocol="jaxa-earth", collection=canonical)

get_catalog() #

Return the dataset map (satisfies the abstract contract).

Returns:

Type Description
dict[str, Dataset]

dict[str, Dataset]: Same object as :attr:datasets.

Examples:

  • Inspect one of the rows by canonical key:
    >>> from earthlens.jaxa import Catalog
    >>> cat = Catalog()
    >>> cat.get_catalog()["aw3d30"].protocol
    'jaxa-earth'
    
Source code in src/earthlens/jaxa/catalog.py
def get_catalog(self) -> dict[str, Dataset]:
    """Return the dataset map (satisfies the abstract contract).

    Returns:
        dict[str, Dataset]: Same object as :attr:`datasets`.

    Examples:
        - Inspect one of the rows by canonical key:
            ```python
            >>> from earthlens.jaxa import Catalog
            >>> cat = Catalog()
            >>> cat.get_catalog()["aw3d30"].protocol
            'jaxa-earth'

            ```
    """
    return self.datasets

load(catalog_path=None) classmethod #

Read and validate the JAXA catalog from disk (cached).

Parameters:

Name Type Description Default
catalog_path Path | None

Path to the catalog directory (the sharded default) or a single YAML file (tests / legacy). Defaults to the module-level :data:CATALOG_PATH.

None

Returns:

Type Description
Catalog

A fully-populated :class:Catalog merged across every

Catalog

contributing file.

Raises:

Type Description
ValueError

If the path has no datasets: rows across any file, or any row fails validation (missing identifier, alias conflict), or a key is declared in two shards.

FileNotFoundError

If catalog_path does not exist (the bundled catalog ships with the wheel, so this only fires when a caller passes an explicit, missing path).

Source code in src/earthlens/jaxa/catalog.py
@classmethod
def load(cls, catalog_path: Path | None = None) -> Catalog:
    """Read and validate the JAXA catalog from disk (cached).

    Args:
        catalog_path: Path to the catalog directory (the sharded
            default) or a single YAML file (tests / legacy). Defaults
            to the module-level :data:`CATALOG_PATH`.

    Returns:
        A fully-populated :class:`Catalog` merged across every
        contributing file.

    Raises:
        ValueError: If the path has no `datasets:` rows across any
            file, or any row fails validation (missing identifier,
            alias conflict), or a key is declared in two shards.
        FileNotFoundError: If `catalog_path` does not exist (the
            bundled catalog ships with the wheel, so this only fires
            when a caller passes an explicit, missing path).
    """
    path = catalog_path if catalog_path is not None else CATALOG_PATH
    files = _yaml_files_for(path)
    try:
        mtime_tuple = tuple((str(f), f.stat().st_mtime_ns) for f in files)
    except FileNotFoundError:
        mtime_tuple = ((str(path.resolve()), 0),)
    cache_key = (str(path.resolve()), mtime_tuple)
    cached = _CATALOG_CACHE.get(cache_key)
    if cached is not None:
        return cached

    merged_rows: dict[str, dict[str, Any]] = {}
    row_origin: dict[str, Path] = {}
    available: list[str] = []
    for file_path in files:
        data = load_yaml_strict(file_path) or {}
        for ident in data.get("available_datasets") or []:
            available.append(str(ident))
        for key, body in (data.get("datasets") or {}).items():
            if key in merged_rows:
                raise ValueError(
                    f"dataset {key!r} declared in two catalog files: "
                    f"{row_origin[key]} and {file_path}"
                )
            merged_rows[key] = dict(body or {})
            row_origin[key] = file_path

    if not merged_rows:
        raise ValueError(
            f"{path} is missing or has an empty 'datasets:' block. "
            "The JAXA catalog must list at least one dataset."
        )
    datasets: dict[str, Dataset] = {}
    for key, body in merged_rows.items():
        try:
            datasets[key] = Dataset(key=key, **body)
        except ValidationError as exc:
            raise ValueError(
                f"{row_origin[key]} dataset {key!r} failed validation:\n{exc}"
            ) from exc
    catalog = cls(datasets=datasets, available_datasets=available)
    _CATALOG_CACHE[cache_key] = catalog
    return catalog

model_post_init(__context) #

Auto-load the bundled catalog when no datasets were supplied.

Catalog() with no args reads :data:CATALOG_PATH (cached by (path, mtime)); passing datasets=... skips the disk read (used in tests). Either way the aliases map is derived from the loaded rows. available_datasets is populated from the YAML's optional available_datasets: block (the refresh CLI's --write target — an informational index of every live SDK id across all three protocols) or, when that block is absent, defaults to the sorted curated keys so the dict-like surface (len(cat) etc.) still works.

Source code in src/earthlens/jaxa/catalog.py
def model_post_init(self, __context: Any) -> None:
    """Auto-load the bundled catalog when no datasets were supplied.

    `Catalog()` with no args reads :data:`CATALOG_PATH` (cached by
    `(path, mtime)`); passing `datasets=...` skips the disk read (used
    in tests). Either way the `aliases` map is derived from the loaded
    rows. `available_datasets` is populated from the YAML's optional
    `available_datasets:` block (the refresh CLI's `--write` target —
    an informational index of every live SDK id across all three
    protocols) or, when that block is absent, defaults to the sorted
    curated keys so the dict-like surface (`len(cat)` etc.) still
    works.
    """
    if not self.datasets:
        loaded = Catalog.load()
        self.datasets = loaded.datasets
        if loaded.available_datasets and not self.available_datasets:
            self.available_datasets = loaded.available_datasets
    # Build the alias index from the rows' `aliases` lists. Conflicts
    # (same alias on two rows) raise; canonical keys also resolve to
    # themselves so `resolve()` is total over both.
    aliases: dict[str, str] = {}
    for key, ds in self.datasets.items():
        aliases[key] = key
        for alias in ds.aliases:
            if alias in aliases and aliases[alias] != key:
                raise ValueError(
                    f"alias {alias!r} is claimed by both {aliases[alias]!r} "
                    f"and {key!r} — pick one."
                )
            aliases[alias] = key
    self.aliases = aliases
    if not self.available_datasets:
        self.available_datasets = sorted(self.datasets)

resolve(key) #

Map a friendly alias / raw id to its canonical key.

Parameters:

Name Type Description Default
key str

A canonical key, friendly alias, raw G-Portal numeric id (e.g. "11001002"), or raw jaxa.earth STAC collection name (e.g. "JAXA.EORC_ALOS.PRISM_AW3D30.v3.2_global").

required

Returns:

Name Type Description
str str

The canonical catalog key. Raw ids pass through unchanged.

Raises:

Type Description
ValueError

When key is neither curated nor a syntactically valid raw id, with a did-you-mean hint drawn from the union of canonical keys and aliases.

Examples:

  • A canonical key resolves to itself; an alias resolves to its row:
    >>> from earthlens.jaxa import Catalog
    >>> cat = Catalog()
    >>> cat.resolve("aw3d30")
    'aw3d30'
    >>> cat.resolve("elevation")
    'aw3d30'
    
  • Raw G-Portal numeric ids pass through unmapped:
    >>> from earthlens.jaxa import Catalog
    >>> Catalog().resolve("11001002")
    '11001002'
    
Source code in src/earthlens/jaxa/catalog.py
def resolve(self, key: str) -> str:
    """Map a friendly alias / raw id to its canonical key.

    Args:
        key: A canonical key, friendly alias, raw G-Portal numeric
            id (e.g. ``"11001002"``), or raw jaxa.earth STAC
            collection name (e.g. ``"JAXA.EORC_ALOS.PRISM_AW3D30.v3.2_global"``).

    Returns:
        str: The canonical catalog key. Raw ids pass through unchanged.

    Raises:
        ValueError: When `key` is neither curated nor a syntactically
            valid raw id, with a did-you-mean hint drawn from the
            union of canonical keys and aliases.

    Examples:
        - A canonical key resolves to itself; an alias resolves to its row:
            ```python
            >>> from earthlens.jaxa import Catalog
            >>> cat = Catalog()
            >>> cat.resolve("aw3d30")
            'aw3d30'
            >>> cat.resolve("elevation")
            'aw3d30'

            ```
        - Raw G-Portal numeric ids pass through unmapped:
            ```python
            >>> from earthlens.jaxa import Catalog
            >>> Catalog().resolve("11001002")
            '11001002'

            ```
    """
    if key in self.aliases:
        return self.aliases[key]
    # Raw G-Portal id (7-9 digits) or raw jaxa.earth STAC collection name
    # passes through unmapped, mirroring `usgs_water.Catalog.resolve` for
    # raw 5-digit NWIS codes. The available_datasets: index lets a
    # curator verify the id is real upstream.
    if _GPORTAL_ID_RE.match(key) or _JAXA_EARTH_PREFIX_RE.match(key):
        return key
    close = difflib.get_close_matches(key, self.aliases, n=1)
    hint = f" Did you mean {close[0]!r}?" if close else ""
    raise ValueError(
        f"{key!r} is not in the JAXA catalog. "
        f"Known keys: {sorted(self.datasets)}.{hint}"
    )

Dataset #

Bases: BaseModel

One JAXA dataset row.

The row's protocol field is the discriminator the backend dispatches on (_jaxa_earth.py vs _gportal.py vs _ptree.py). The cross-field validator enforces that the right identifier is present for each protocol:

  • protocol="jaxa-earth" requires collection (the STAC collection name the jaxa.earth API consumes, e.g. "JAXA.EORC_ALOS.PRISM_AW3D30.v3.2_global").
  • protocol="gportal" requires short_name (the G-Portal numeric dataset id, e.g. "10003001").
  • protocol="ptree" requires short_name (the P-Tree product token, e.g. "himawari-ahi-fldk").

Attributes:

Name Type Description
key str

Canonical catalog key ("aw3d30") — repeated on the row so it travels with the object outside the catalog dict.

protocol JaxaProtocol

One of "jaxa-earth", "gportal", or "ptree".

collection str | None

STAC collection name. Required when protocol="jaxa-earth"; must be None for gportal and ptree.

short_name str | None

Protocol-specific product identifier — a G-Portal numeric dataset id (e.g. "10003001") for gportal, or a P-Tree product token (e.g. "himawari-ahi-fldk") for ptree. Required for both credentialed protocols; must be None for jaxa-earth.

default_band str | None

The band selected when the user does not pass bands=. Used by jaxa-earth (picks the GeoTIFF band to write) and ptree (picks which Himawari AHI band to download from B01..B16). Ignored for gportal (G-Portal products are downloaded whole).

aliases list[str]

Friendly keys that resolve to this row's canonical key (e.g. ["elevation", "dem"] for "aw3d30").

description str

Human-readable summary used in docs.

Examples:

  • A jaxa-earth row needs collection:
    >>> from earthlens.jaxa.catalog import Dataset
    >>> ds = Dataset(
    ...     key="aw3d30",
    ...     protocol="jaxa-earth",
    ...     collection="JAXA.EORC_ALOS.PRISM_AW3D30.v3.2_global",
    ...     default_band="DSM",
    ... )
    >>> ds.protocol
    'jaxa-earth'
    
  • A gportal row needs short_name:
    >>> from earthlens.jaxa.catalog import Dataset
    >>> ds = Dataset(key="sgli-l380", protocol="gportal", short_name="10003001")
    >>> ds.short_name
    '10003001'
    
Source code in src/earthlens/jaxa/catalog.py
class Dataset(BaseModel):
    """One JAXA dataset row.

    The row's `protocol` field is the discriminator the backend dispatches
    on (`_jaxa_earth.py` vs `_gportal.py` vs `_ptree.py`). The cross-field
    validator enforces that the right identifier is present for each
    protocol:

    * `protocol="jaxa-earth"` requires `collection` (the STAC collection
      name the `jaxa.earth` API consumes, e.g.
      `"JAXA.EORC_ALOS.PRISM_AW3D30.v3.2_global"`).
    * `protocol="gportal"` requires `short_name` (the G-Portal numeric
      dataset id, e.g. `"10003001"`).
    * `protocol="ptree"` requires `short_name` (the P-Tree product
      token, e.g. `"himawari-ahi-fldk"`).

    Attributes:
        key: Canonical catalog key (`"aw3d30"`) — repeated on the row so it
            travels with the object outside the catalog dict.
        protocol: One of `"jaxa-earth"`, `"gportal"`, or `"ptree"`.
        collection: STAC collection name. Required when
            `protocol="jaxa-earth"`; must be `None` for `gportal` and
            `ptree`.
        short_name: Protocol-specific product identifier — a G-Portal
            numeric dataset id (e.g. `"10003001"`) for `gportal`, or a
            P-Tree product token (e.g. `"himawari-ahi-fldk"`) for
            `ptree`. Required for both credentialed protocols; must be
            `None` for `jaxa-earth`.
        default_band: The band selected when the user does not pass
            `bands=`. Used by `jaxa-earth` (picks the GeoTIFF band to
            write) and `ptree` (picks which Himawari AHI band to
            download from `B01`..`B16`). Ignored for `gportal`
            (G-Portal products are downloaded whole).
        aliases: Friendly keys that resolve to this row's canonical key
            (e.g. `["elevation", "dem"]` for `"aw3d30"`).
        description: Human-readable summary used in docs.

    Examples:
        - A `jaxa-earth` row needs `collection`:
            ```python
            >>> from earthlens.jaxa.catalog import Dataset
            >>> ds = Dataset(
            ...     key="aw3d30",
            ...     protocol="jaxa-earth",
            ...     collection="JAXA.EORC_ALOS.PRISM_AW3D30.v3.2_global",
            ...     default_band="DSM",
            ... )
            >>> ds.protocol
            'jaxa-earth'

            ```
        - A `gportal` row needs `short_name`:
            ```python
            >>> from earthlens.jaxa.catalog import Dataset
            >>> ds = Dataset(key="sgli-l380", protocol="gportal", short_name="10003001")
            >>> ds.short_name
            '10003001'

            ```
    """

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

    key: str
    protocol: JaxaProtocol
    collection: str | None = None
    short_name: str | None = None
    default_band: str | None = None
    aliases: list[str] = Field(default_factory=list)
    description: str = ""

    @model_validator(mode="after")
    def _check_protocol_identifier(self) -> Dataset:
        """Enforce one identifier per protocol.

        Raises:
            ValueError: If a `jaxa-earth` row has no `collection`, or a
                credentialed (`gportal` / `ptree`) row has no
                `short_name`, or the row sets the identifier belonging
                to the other side (a `jaxa-earth` row with a
                `short_name`, or a credentialed row with a
                `collection`).
        """
        if self.protocol == "jaxa-earth":
            if not self.collection:
                raise ValueError(
                    f"dataset {self.key!r} has protocol='jaxa-earth' but no "
                    "`collection` (the STAC collection name) — set it."
                )
            if self.short_name is not None:
                raise ValueError(
                    f"dataset {self.key!r} has protocol='jaxa-earth'; "
                    "`short_name` belongs to a credentialed protocol "
                    "(gportal/ptree) — drop it."
                )
        else:
            if not self.short_name:
                raise ValueError(
                    f"dataset {self.key!r} has protocol={self.protocol!r} "
                    "but no `short_name` (the protocol's product identifier) "
                    "— set it."
                )
            if self.collection is not None:
                raise ValueError(
                    f"dataset {self.key!r} has protocol={self.protocol!r}; "
                    "`collection` belongs to the jaxa-earth protocol "
                    "— drop it."
                )
        return self

clear_catalog_cache() #

Empty the module-level JAXA catalog parse cache.

Examples:

  • Calling it is a no-op when the cache is already empty, and subsequent Catalog() calls re-read the YAML from disk:
    >>> from earthlens.jaxa.catalog import clear_catalog_cache, Catalog
    >>> clear_catalog_cache()
    >>> "aw3d30" in Catalog()
    True
    
Source code in src/earthlens/jaxa/catalog.py
def clear_catalog_cache() -> None:
    """Empty the module-level JAXA catalog parse cache.

    Examples:
        - Calling it is a no-op when the cache is already empty, and
          subsequent `Catalog()` calls re-read the YAML from disk:
            ```python
            >>> from earthlens.jaxa.catalog import clear_catalog_cache, Catalog
            >>> clear_catalog_cache()
            >>> "aw3d30" in Catalog()
            True

            ```
    """
    _CATALOG_CACHE.clear()

earthlens.jaxa.auth #

Credentials and resolution for the JAXA backend's credentialed branches.

JAXA's archive is reached through three protocols, two of which need credentials:

  • protocol: jaxa-earth — open STAC + COG access through the official jaxa.earth API. Authless.
  • protocol: gportal — G-Portal mission archive accessed via SFTP through the community gportal SDK. Needs a free G-Portal account.
  • protocol: ptree — near-real-time Himawari-8/9 HSD granules on ftp.ptree.jaxa.jp (30-day rolling archive). Needs a free P-Tree account, distinct from G-Portal registration.

JaxaAuth mirrors OpenaqAuth (optional secret resolved from explicit kwargs or environment variables) but binds the target protocol at construction time so the parent's no-arg AbstractAuth.configure() / is_authenticated() — the contract AbstractDataSource.authenticate() calls — does the right thing per protocol:

  • JaxaAuth(creds, protocol="jaxa-earth") makes configure() a no-op (no credentials needed).
  • JaxaAuth(creds, protocol="gportal") makes configure() resolve and store the username + password from explicit credentials or from $GPORTAL_USERNAME / $GPORTAL_PASSWORD, raising :class:AuthenticationError on miss.
  • JaxaAuth(creds, protocol="ptree") does the same for P-Tree, reading ptree_username / ptree_password off the credentials or falling back to $JAXA_PTREE_USERNAME / $JAXA_PTREE_PASSWORD. The two credential pairs never share values — P-Tree registration is separate from G-Portal.

The resolved username / password are exposed as :attr:JaxaAuth.username and :attr:JaxaAuth.password (the latter is a :class:pydantic.SecretStr) so each credentialed branch can pass them straight to its transport client as kwargs — no module-level mutation of the SDK is required.

AuthenticationError #

Bases: AuthenticationError

Raised when a credentialed JAXA branch has no usable credentials.

Raised by the gportal and ptree branches; the jaxa-earth branch is authless. The message names the exact fix for the missing pair: for gportal, pass gportal_username= / gportal_password= to JAXA(...) or set $GPORTAL_USERNAME / $GPORTAL_PASSWORD; for ptree, pass ptree_username= / ptree_password= or set $JAXA_PTREE_USERNAME / $JAXA_PTREE_PASSWORD.

Source code in src/earthlens/jaxa/auth.py
class AuthenticationError(_BaseAuthenticationError):
    """Raised when a credentialed JAXA branch has no usable credentials.

    Raised by the `gportal` and `ptree` branches; the `jaxa-earth` branch is
    authless. The message names the exact fix for the missing pair: for
    `gportal`, pass `gportal_username=` / `gportal_password=` to `JAXA(...)`
    or set `$GPORTAL_USERNAME` / `$GPORTAL_PASSWORD`; for `ptree`, pass
    `ptree_username=` / `ptree_password=` or set `$JAXA_PTREE_USERNAME` /
    `$JAXA_PTREE_PASSWORD`.
    """

JaxaAuth #

Bases: AbstractAuth[JaxaCredentials]

Resolve credentials for the bound JAXA protocol.

The class is optional-credentials: the protocol is bound at construction so the parent contract's no-arg configure() / is_authenticated() (the methods AbstractDataSource.authenticate() calls) act on the right side. JaxaAuth(creds, protocol="jaxa-earth") makes configure() a no-op; JaxaAuth(creds, protocol="gportal") or JaxaAuth(creds, protocol="ptree") resolves the matching username + password from explicit credentials or the protocol's env-var pair, raising :class:AuthenticationError on miss.

After a successful configure() on a credentialed protocol, the resolved values are available via :attr:username and :attr:password so the branch can pass them straight into its transport client (gportal.download(username=, password=), ftplib.FTP.login(...), paramiko.Transport.connect(...)) instead of mutating any SDK's module-level globals.

Attributes:

Name Type Description
_creds

The :class:JaxaCredentials passed at construction.

_protocol JaxaProtocol

The bound protocol — one of "jaxa-earth", "gportal" or "ptree".

Examples:

  • The jaxa-earth protocol never needs credentials:
    >>> from earthlens.jaxa import JaxaAuth, JaxaCredentials
    >>> auth = JaxaAuth(JaxaCredentials(), protocol="jaxa-earth")
    >>> auth.configure()
    >>> auth.is_authenticated()
    True
    
Source code in src/earthlens/jaxa/auth.py
class JaxaAuth(AbstractAuth[JaxaCredentials]):
    """Resolve credentials for the bound JAXA protocol.

    The class is optional-credentials: the protocol is **bound at
    construction** so the parent contract's no-arg `configure()` /
    `is_authenticated()` (the methods `AbstractDataSource.authenticate()`
    calls) act on the right side. `JaxaAuth(creds, protocol="jaxa-earth")`
    makes `configure()` a no-op; `JaxaAuth(creds, protocol="gportal")` or
    `JaxaAuth(creds, protocol="ptree")` resolves the matching username +
    password from explicit credentials or the protocol's env-var pair,
    raising :class:`AuthenticationError` on miss.

    After a successful `configure()` on a credentialed protocol, the
    resolved values are available via :attr:`username` and
    :attr:`password` so the branch can pass them straight into its
    transport client (`gportal.download(username=, password=)`,
    `ftplib.FTP.login(...)`, `paramiko.Transport.connect(...)`) instead of
    mutating any SDK's module-level globals.

    Attributes:
        _creds: The :class:`JaxaCredentials` passed at construction.
        _protocol: The bound protocol — one of `"jaxa-earth"`, `"gportal"`
            or `"ptree"`.

    Examples:
        - The `jaxa-earth` protocol never needs credentials:
            ```python
            >>> from earthlens.jaxa import JaxaAuth, JaxaCredentials
            >>> auth = JaxaAuth(JaxaCredentials(), protocol="jaxa-earth")
            >>> auth.configure()
            >>> auth.is_authenticated()
            True

            ```
    """

    def __init__(
        self,
        credentials: JaxaCredentials,
        protocol: JaxaProtocol = "jaxa-earth",
    ) -> None:
        """Bind the protocol; does not resolve credentials yet.

        Args:
            credentials: The :class:`JaxaCredentials` value object carrying
                the optional G-Portal and P-Tree username / password pairs.
            protocol: The protocol this auth instance targets. The
                parent's no-arg :meth:`configure` dispatches on this so
                `AbstractDataSource.authenticate()` fails fast for missing
                credentials on either credentialed branch.

        Raises:
            ValueError: When `protocol` is not one of the three supported
                values.
        """
        if protocol not in ("jaxa-earth", "gportal", "ptree"):
            raise ValueError(
                "protocol must be one of 'jaxa-earth', 'gportal', 'ptree'; "
                f"got {protocol!r}."
            )
        super().__init__(credentials)
        self._protocol: JaxaProtocol = protocol
        self._configured = False
        self._username: str | None = None
        self._password: SecretStr | None = None

    @property
    def protocol(self) -> JaxaProtocol:
        """The protocol bound at construction."""
        return self._protocol

    @property
    def username(self) -> str | None:
        """The resolved username, or `None` until `configure()` runs."""
        return self._username

    @property
    def password(self) -> SecretStr | None:
        """The resolved password (still wrapped in `SecretStr`).

        Returns `None` until :meth:`configure` runs. Callers that need
        the raw string call `.get_secret_value()` themselves at the call
        site — the wrapper stays on `JaxaAuth` so it never lands in a
        `repr` or a log line.
        """
        return self._password

    def configure(self) -> None:
        """Resolve credentials for the bound protocol.

        For `"jaxa-earth"` the call is a no-op (the JAXA Earth API needs
        no auth). For `"gportal"` and `"ptree"` the call reads the
        explicit credentials (preferred) or the protocol's environment
        variables (`$GPORTAL_USERNAME` / `$GPORTAL_PASSWORD` and
        `$JAXA_PTREE_USERNAME` / `$JAXA_PTREE_PASSWORD` respectively) as
        fallback, and caches them on the instance for the branch to read
        through :attr:`username` / :attr:`password`. Idempotent — a
        second call after `is_authenticated()` returns `True`
        short-circuits.

        Raises:
            AuthenticationError: When the protocol is credentialed and
                neither the explicit credentials nor the environment
                variables supply a usable username + password pair. The
                message names the env vars and the free-registration URL
                for the protocol that failed to resolve.

        Examples:
            - The jaxa-earth protocol's configure is a no-op:
                ```python
                >>> from earthlens.jaxa import JaxaAuth, JaxaCredentials
                >>> auth = JaxaAuth(JaxaCredentials(), protocol="jaxa-earth")
                >>> auth.configure()
                >>> auth.is_authenticated()
                True

                ```
            - With gportal credentials supplied explicitly, configure caches them
              for the branch to read via `.username` / `.password`:
                ```python
                >>> from pydantic import SecretStr
                >>> from earthlens.jaxa import JaxaAuth, JaxaCredentials
                >>> creds = JaxaCredentials(
                ...     gportal_username="alice",
                ...     gportal_password=SecretStr("topsecret"),
                ... )
                >>> auth = JaxaAuth(creds, protocol="gportal")
                >>> auth.configure()
                >>> auth.username
                'alice'

                ```
            - The `ptree` protocol reads its own credential pair:
                ```python
                >>> from pydantic import SecretStr
                >>> from earthlens.jaxa import JaxaAuth, JaxaCredentials
                >>> creds = JaxaCredentials(
                ...     ptree_username="alice@example.org",
                ...     ptree_password=SecretStr("hunter2"),
                ... )
                >>> auth = JaxaAuth(creds, protocol="ptree")
                >>> auth.configure()
                >>> auth.username
                'alice@example.org'

                ```
        """
        if self._configured:
            return
        if self._protocol == "jaxa-earth":
            self._configured = True
            return
        spec = _PROTOCOL_SPECS[self._protocol]
        cred_username = getattr(self._creds, spec.cred_user)
        cred_password: SecretStr | None = getattr(self._creds, spec.cred_pass)
        username = cred_username or os.environ.get(spec.env_user)
        password_raw = (
            cred_password.get_secret_value()
            if cred_password is not None
            else os.environ.get(spec.env_pass)
        )
        if not username or not password_raw:
            raise AuthenticationError(
                f"no {spec.human_name} credentials available: pass "
                f"{spec.cred_user}= and {spec.cred_pass}= to JAXA(...), "
                f"or set both {spec.env_user} and {spec.env_pass} "
                f"environment variables. Register a free account at "
                f"{spec.register_url}."
            )
        self._username = username
        self._password = SecretStr(password_raw)
        self._configured = True

    def is_authenticated(self) -> bool:
        """Return `True` once :meth:`configure` has run successfully.

        Returns:
            bool: `True` after :meth:`configure` has completed without
                raising; `False` before then.

        Examples:
            - Construction does not authenticate; configure flips the flag:
                ```python
                >>> from earthlens.jaxa import JaxaAuth, JaxaCredentials
                >>> auth = JaxaAuth(JaxaCredentials(), protocol="jaxa-earth")
                >>> auth.is_authenticated()
                False
                >>> auth.configure()
                >>> auth.is_authenticated()
                True

                ```
        """
        return self._configured

password property #

The resolved password (still wrapped in SecretStr).

Returns None until :meth:configure runs. Callers that need the raw string call .get_secret_value() themselves at the call site — the wrapper stays on JaxaAuth so it never lands in a repr or a log line.

protocol property #

The protocol bound at construction.

username property #

The resolved username, or None until configure() runs.

__init__(credentials, protocol='jaxa-earth') #

Bind the protocol; does not resolve credentials yet.

Parameters:

Name Type Description Default
credentials JaxaCredentials

The :class:JaxaCredentials value object carrying the optional G-Portal and P-Tree username / password pairs.

required
protocol JaxaProtocol

The protocol this auth instance targets. The parent's no-arg :meth:configure dispatches on this so AbstractDataSource.authenticate() fails fast for missing credentials on either credentialed branch.

'jaxa-earth'

Raises:

Type Description
ValueError

When protocol is not one of the three supported values.

Source code in src/earthlens/jaxa/auth.py
def __init__(
    self,
    credentials: JaxaCredentials,
    protocol: JaxaProtocol = "jaxa-earth",
) -> None:
    """Bind the protocol; does not resolve credentials yet.

    Args:
        credentials: The :class:`JaxaCredentials` value object carrying
            the optional G-Portal and P-Tree username / password pairs.
        protocol: The protocol this auth instance targets. The
            parent's no-arg :meth:`configure` dispatches on this so
            `AbstractDataSource.authenticate()` fails fast for missing
            credentials on either credentialed branch.

    Raises:
        ValueError: When `protocol` is not one of the three supported
            values.
    """
    if protocol not in ("jaxa-earth", "gportal", "ptree"):
        raise ValueError(
            "protocol must be one of 'jaxa-earth', 'gportal', 'ptree'; "
            f"got {protocol!r}."
        )
    super().__init__(credentials)
    self._protocol: JaxaProtocol = protocol
    self._configured = False
    self._username: str | None = None
    self._password: SecretStr | None = None

configure() #

Resolve credentials for the bound protocol.

For "jaxa-earth" the call is a no-op (the JAXA Earth API needs no auth). For "gportal" and "ptree" the call reads the explicit credentials (preferred) or the protocol's environment variables ($GPORTAL_USERNAME / $GPORTAL_PASSWORD and $JAXA_PTREE_USERNAME / $JAXA_PTREE_PASSWORD respectively) as fallback, and caches them on the instance for the branch to read through :attr:username / :attr:password. Idempotent — a second call after is_authenticated() returns True short-circuits.

Raises:

Type Description
AuthenticationError

When the protocol is credentialed and neither the explicit credentials nor the environment variables supply a usable username + password pair. The message names the env vars and the free-registration URL for the protocol that failed to resolve.

Examples:

  • The jaxa-earth protocol's configure is a no-op:
    >>> from earthlens.jaxa import JaxaAuth, JaxaCredentials
    >>> auth = JaxaAuth(JaxaCredentials(), protocol="jaxa-earth")
    >>> auth.configure()
    >>> auth.is_authenticated()
    True
    
  • With gportal credentials supplied explicitly, configure caches them for the branch to read via .username / .password:
    >>> from pydantic import SecretStr
    >>> from earthlens.jaxa import JaxaAuth, JaxaCredentials
    >>> creds = JaxaCredentials(
    ...     gportal_username="alice",
    ...     gportal_password=SecretStr("topsecret"),
    ... )
    >>> auth = JaxaAuth(creds, protocol="gportal")
    >>> auth.configure()
    >>> auth.username
    'alice'
    
  • The ptree protocol reads its own credential pair:
    >>> from pydantic import SecretStr
    >>> from earthlens.jaxa import JaxaAuth, JaxaCredentials
    >>> creds = JaxaCredentials(
    ...     ptree_username="alice@example.org",
    ...     ptree_password=SecretStr("hunter2"),
    ... )
    >>> auth = JaxaAuth(creds, protocol="ptree")
    >>> auth.configure()
    >>> auth.username
    'alice@example.org'
    
Source code in src/earthlens/jaxa/auth.py
def configure(self) -> None:
    """Resolve credentials for the bound protocol.

    For `"jaxa-earth"` the call is a no-op (the JAXA Earth API needs
    no auth). For `"gportal"` and `"ptree"` the call reads the
    explicit credentials (preferred) or the protocol's environment
    variables (`$GPORTAL_USERNAME` / `$GPORTAL_PASSWORD` and
    `$JAXA_PTREE_USERNAME` / `$JAXA_PTREE_PASSWORD` respectively) as
    fallback, and caches them on the instance for the branch to read
    through :attr:`username` / :attr:`password`. Idempotent — a
    second call after `is_authenticated()` returns `True`
    short-circuits.

    Raises:
        AuthenticationError: When the protocol is credentialed and
            neither the explicit credentials nor the environment
            variables supply a usable username + password pair. The
            message names the env vars and the free-registration URL
            for the protocol that failed to resolve.

    Examples:
        - The jaxa-earth protocol's configure is a no-op:
            ```python
            >>> from earthlens.jaxa import JaxaAuth, JaxaCredentials
            >>> auth = JaxaAuth(JaxaCredentials(), protocol="jaxa-earth")
            >>> auth.configure()
            >>> auth.is_authenticated()
            True

            ```
        - With gportal credentials supplied explicitly, configure caches them
          for the branch to read via `.username` / `.password`:
            ```python
            >>> from pydantic import SecretStr
            >>> from earthlens.jaxa import JaxaAuth, JaxaCredentials
            >>> creds = JaxaCredentials(
            ...     gportal_username="alice",
            ...     gportal_password=SecretStr("topsecret"),
            ... )
            >>> auth = JaxaAuth(creds, protocol="gportal")
            >>> auth.configure()
            >>> auth.username
            'alice'

            ```
        - The `ptree` protocol reads its own credential pair:
            ```python
            >>> from pydantic import SecretStr
            >>> from earthlens.jaxa import JaxaAuth, JaxaCredentials
            >>> creds = JaxaCredentials(
            ...     ptree_username="alice@example.org",
            ...     ptree_password=SecretStr("hunter2"),
            ... )
            >>> auth = JaxaAuth(creds, protocol="ptree")
            >>> auth.configure()
            >>> auth.username
            'alice@example.org'

            ```
    """
    if self._configured:
        return
    if self._protocol == "jaxa-earth":
        self._configured = True
        return
    spec = _PROTOCOL_SPECS[self._protocol]
    cred_username = getattr(self._creds, spec.cred_user)
    cred_password: SecretStr | None = getattr(self._creds, spec.cred_pass)
    username = cred_username or os.environ.get(spec.env_user)
    password_raw = (
        cred_password.get_secret_value()
        if cred_password is not None
        else os.environ.get(spec.env_pass)
    )
    if not username or not password_raw:
        raise AuthenticationError(
            f"no {spec.human_name} credentials available: pass "
            f"{spec.cred_user}= and {spec.cred_pass}= to JAXA(...), "
            f"or set both {spec.env_user} and {spec.env_pass} "
            f"environment variables. Register a free account at "
            f"{spec.register_url}."
        )
    self._username = username
    self._password = SecretStr(password_raw)
    self._configured = True

is_authenticated() #

Return True once :meth:configure has run successfully.

Returns:

Name Type Description
bool bool

True after :meth:configure has completed without raising; False before then.

Examples:

  • Construction does not authenticate; configure flips the flag:
    >>> from earthlens.jaxa import JaxaAuth, JaxaCredentials
    >>> auth = JaxaAuth(JaxaCredentials(), protocol="jaxa-earth")
    >>> auth.is_authenticated()
    False
    >>> auth.configure()
    >>> auth.is_authenticated()
    True
    
Source code in src/earthlens/jaxa/auth.py
def is_authenticated(self) -> bool:
    """Return `True` once :meth:`configure` has run successfully.

    Returns:
        bool: `True` after :meth:`configure` has completed without
            raising; `False` before then.

    Examples:
        - Construction does not authenticate; configure flips the flag:
            ```python
            >>> from earthlens.jaxa import JaxaAuth, JaxaCredentials
            >>> auth = JaxaAuth(JaxaCredentials(), protocol="jaxa-earth")
            >>> auth.is_authenticated()
            False
            >>> auth.configure()
            >>> auth.is_authenticated()
            True

            ```
    """
    return self._configured

JaxaCredentials #

Bases: BaseModel

Frozen value object holding the credentialed-branch secrets.

All fields are optional at construction time: None means "resolve from the corresponding environment variable at :meth:JaxaAuth.configure time". The real "are credentials available?" gate is :meth:JaxaAuth.configure, not this model.

G-Portal and P-Tree credentials are stored side-by-side because a single :class:JAXA instance is bound to one protocol per call — the unused pair is simply ignored.

Attributes:

Name Type Description
gportal_username str | None

G-Portal username. None defers to $GPORTAL_USERNAME at configure time.

gportal_password SecretStr | None

G-Portal password, stored as a SecretStr so it is never echoed in repr or logs. None defers to $GPORTAL_PASSWORD.

ptree_username str | None

P-Tree username (the registered email). None defers to $JAXA_PTREE_USERNAME.

ptree_password SecretStr | None

P-Tree password, stored as a SecretStr. None defers to $JAXA_PTREE_PASSWORD.

Examples:

  • The password is hidden in repr:
    >>> from earthlens.jaxa import JaxaCredentials
    >>> creds = JaxaCredentials(gportal_username="alice", gportal_password="topsecret")
    >>> "topsecret" in repr(creds)
    False
    >>> creds.gportal_password.get_secret_value()
    'topsecret'
    
  • The same protection applies to the P-Tree password:
    >>> from earthlens.jaxa import JaxaCredentials
    >>> creds = JaxaCredentials(
    ...     ptree_username="alice@example.org",
    ...     ptree_password="hunter2",
    ... )
    >>> "hunter2" in repr(creds)
    False
    
  • Every field is optional — rely on the environment instead:
    >>> from earthlens.jaxa import JaxaCredentials
    >>> JaxaCredentials().ptree_username is None
    True
    
Source code in src/earthlens/jaxa/auth.py
class JaxaCredentials(BaseModel):
    """Frozen value object holding the credentialed-branch secrets.

    All fields are optional at construction time: `None` means "resolve
    from the corresponding environment variable at :meth:`JaxaAuth.configure`
    time". The real "are credentials available?" gate is
    :meth:`JaxaAuth.configure`, not this model.

    G-Portal and P-Tree credentials are stored side-by-side because a
    single :class:`JAXA` instance is bound to one protocol per call — the
    unused pair is simply ignored.

    Attributes:
        gportal_username: G-Portal username. `None` defers to
            `$GPORTAL_USERNAME` at configure time.
        gportal_password: G-Portal password, stored as a `SecretStr` so it
            is never echoed in `repr` or logs. `None` defers to
            `$GPORTAL_PASSWORD`.
        ptree_username: P-Tree username (the registered email). `None`
            defers to `$JAXA_PTREE_USERNAME`.
        ptree_password: P-Tree password, stored as a `SecretStr`. `None`
            defers to `$JAXA_PTREE_PASSWORD`.

    Examples:
        - The password is hidden in `repr`:
            ```python
            >>> from earthlens.jaxa import JaxaCredentials
            >>> creds = JaxaCredentials(gportal_username="alice", gportal_password="topsecret")
            >>> "topsecret" in repr(creds)
            False
            >>> creds.gportal_password.get_secret_value()
            'topsecret'

            ```
        - The same protection applies to the P-Tree password:
            ```python
            >>> from earthlens.jaxa import JaxaCredentials
            >>> creds = JaxaCredentials(
            ...     ptree_username="alice@example.org",
            ...     ptree_password="hunter2",
            ... )
            >>> "hunter2" in repr(creds)
            False

            ```
        - Every field is optional — rely on the environment instead:
            ```python
            >>> from earthlens.jaxa import JaxaCredentials
            >>> JaxaCredentials().ptree_username is None
            True

            ```
    """

    model_config = ConfigDict(frozen=True)

    gportal_username: str | None = None
    gportal_password: SecretStr | None = None
    ptree_username: str | None = None
    ptree_password: SecretStr | None = None