Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 25 additions & 5 deletions eodag/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,8 @@
# depends on numpy and ship with a pre-built version of numpy that is older than 1.15.1 (where the warning is silenced
# exactly as below)
"""EODAG package"""
from importlib.metadata import PackageNotFoundError, version

from .api.core import EODataAccessGateway
from .api.product import EOProduct
from .api.search_result import SearchResult
from .utils.logging import setup_logging
from importlib.metadata import PackageNotFoundError, version

try:
__version__ = version(__name__)
Expand All @@ -41,3 +37,27 @@
"SearchResult",
"setup_logging",
]

# Lazy imports (PEP 562) — avoid loading heavy dependencies on ``import eodag``
_LAZY_IMPORTS: dict[str, tuple[str, str]] = {
"EODataAccessGateway": (".api.core", "EODataAccessGateway"),
"EOProduct": (".api.product", "EOProduct"),
"SearchResult": (".api.search_result", "SearchResult"),
"setup_logging": (".utils.logging", "setup_logging"),
}


def __getattr__(name: str):
if name in _LAZY_IMPORTS:
from importlib import import_module

module_path, attr_name = _LAZY_IMPORTS[name]
module = import_module(module_path, __name__)
value = getattr(module, attr_name)
globals()[name] = value
return value
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


def __dir__():
return __all__ + ["__version__"]
5 changes: 4 additions & 1 deletion eodag/api/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@
ValidationError,
)
from eodag.utils.free_text_search import compile_free_text_query
from eodag.utils.stac_reader import fetch_stac_items

if TYPE_CHECKING:
from concurrent.futures import ThreadPoolExecutor
Expand Down Expand Up @@ -2075,6 +2074,8 @@ def serialize(
return filename
collections = set(p.collection for p in search_result)
for collection in collections:
if collection is None:
continue
collection_obj = search_result._dag.collections_config.get(
collection, Collection(id=collection)
)
Expand Down Expand Up @@ -2393,6 +2394,8 @@ def import_stac_items(self, items_urls: list[str]) -> SearchResult:
:param items_urls: A list of STAC items URLs to import
:returns: A SearchResult containing the imported STAC items
"""
from eodag.utils.stac_reader import fetch_stac_items

json_items = []
for item_url in items_urls:
json_items.extend(fetch_stac_items(item_url))
Expand Down
56 changes: 46 additions & 10 deletions eodag/api/product/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,57 @@
#
"""EODAG product package"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any, Optional

if TYPE_CHECKING:
from eodag.api.product._assets import Asset, AssetsDict
from eodag.api.product._product import EOProduct
from eodag.plugins.manager import PluginManager

try:
# import from eodag-cube if installed
from eodag_cube.api.product import ( # pyright: ignore[reportMissingImports]
Asset,
AssetsDict,
EOProduct,
)
except ImportError:
from ._assets import Asset, AssetsDict # type: ignore[assignment]
from ._product import EOProduct # type: ignore[assignment]
# exportable content
__all__ = ["Asset", "AssetsDict", "EOProduct", "unregistered_product_from_item"]

# Lazy imports (PEP 562) — defer heavy _product / _assets loading
_LAZY_IMPORTS: dict[str, tuple[str, str]] = {
"EOProduct": ("._product", "EOProduct"),
"Asset": ("._assets", "Asset"),
"AssetsDict": ("._assets", "AssetsDict"),
}


def _resolve(name: str):
"""Resolve a lazy import, trying eodag-cube first for EOProduct/Asset/AssetsDict."""
module_path, attr_name = _LAZY_IMPORTS[name]
from importlib import import_module

# Try eodag_cube first (provides extended classes with xarray/rasterio support).
# Users just need ``pip install eodag_cube`` — no other change required.
try:
cube_module = import_module("eodag_cube.api.product")
value = getattr(cube_module, attr_name, None)
if value is not None:
globals()[name] = value
return value
except ImportError:
pass

# Fallback to eodag's own modules
module = import_module(module_path, __name__)
value = getattr(module, attr_name)
globals()[name] = value
return value


def __getattr__(name: str):
if name in _LAZY_IMPORTS:
return _resolve(name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


def __dir__():
return __all__


def unregistered_product_from_item(
Expand Down
3 changes: 2 additions & 1 deletion eodag/api/product/_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,8 @@ def download(self, **kwargs: Unpack[DownloadConf]) -> str:
:param kwargs: (optional) Additional named-arguments passed to `plugin.download()`
:returns: The absolute path to the downloaded product on the local filesystem
"""
return self.product.download(asset=self.key, **kwargs)
kwargs["asset"] = self.key
return self.product.download(**kwargs)

def _repr_html_(self):
thead = f"""<thead><tr><td style='text-align: left; color: grey;'>
Expand Down
70 changes: 42 additions & 28 deletions eodag/api/product/_product.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,49 +26,25 @@
from typing import TYPE_CHECKING, Any, Iterable, Optional, Union, cast

import orjson
import requests
from requests import RequestException
from requests.auth import AuthBase
from shapely import geometry
from shapely.errors import ShapelyError

from eodag.types.queryables import CommonStacMetadata
from eodag.types.stac_metadata import create_stac_metadata_model

try:
# import from eodag-cube if installed
from eodag_cube.api.product import ( # pyright: ignore[reportMissingImports]
AssetsDict,
)
except ImportError:
from eodag.api.product._assets import AssetsDict

from eodag.api.product.drivers import DRIVERS
from eodag.api.product.drivers.generic import GenericDriver
from eodag.api.product.metadata_mapping import (
DEFAULT_GEOMETRY,
NOT_AVAILABLE,
NOT_MAPPED,
ONLINE_STATUS,
)
from eodag.utils import (
DEFAULT_DOWNLOAD_TIMEOUT,
DEFAULT_DOWNLOAD_WAIT,
DEFAULT_SHAPELY_GEOMETRY,
DEFAULT_STREAM_REQUESTS_TIMEOUT,
STAC_VERSION,
USER_AGENT,
ProgressCallback,
format_string,
get_geometry_from_various,
)
from eodag.utils.exceptions import DownloadError, MisconfiguredError, ValidationError
from eodag.utils.repr import dict_to_html_table

if TYPE_CHECKING:
import requests
from concurrent.futures import ThreadPoolExecutor
from requests.auth import AuthBase
from shapely.geometry.base import BaseGeometry

from eodag.api.product._assets import AssetsDict
from eodag.api.product.drivers.base import DatasetDriver
from eodag.plugins.apis.base import Api
from eodag.plugins.authentication.base import Authentication
Expand All @@ -78,6 +54,17 @@
from eodag.utils import Unpack


def _get_assets_dict_cls():
"""Get AssetsDict class, preferring eodag-cube if installed."""
try:
from eodag_cube.api.product import ( # pyright: ignore[reportMissingImports]
AssetsDict,
)
except ImportError:
from eodag.api.product._assets import AssetsDict
return AssetsDict


logger = logging.getLogger("eodag.product")


Expand Down Expand Up @@ -134,13 +121,23 @@ class EOProduct:
def __init__(
self, provider: str, properties: dict[str, Any], **kwargs: Any
) -> None:
from shapely.errors import ShapelyError

from eodag.api.product.metadata_mapping import (
DEFAULT_GEOMETRY,
NOT_AVAILABLE,
NOT_MAPPED,
)
from eodag.utils import DEFAULT_SHAPELY_GEOMETRY, get_geometry_from_various

self.provider = provider
self.collection = (
kwargs.get("collection")
or properties.pop("collection", None)
or properties.get("_collection")
)
self.location = self.remote_location = properties.get("eodag:download_link", "")
AssetsDict = _get_assets_dict_cls()
self.assets = AssetsDict(self)
self.properties = {
key: value
Expand Down Expand Up @@ -215,6 +212,9 @@ def as_dict(self) -> dict[str, Any]:
:returns: The representation of a :class:`~eodag.api.product._product.EOProduct` as a
Python dict
"""
from eodag.types.queryables import CommonStacMetadata
from eodag.types.stac_metadata import create_stac_metadata_model

search_intersection = None
if self.search_intersection is not None:
search_intersection = orjson.loads(
Expand Down Expand Up @@ -296,6 +296,8 @@ def from_geojson(cls, feature: dict[str, Any]) -> EOProduct:
:raises: :class:`~eodag.utils.exceptions.ValidationError`
"""
try:
from shapely import geometry

collection = feature.get("collection")
properties = feature["properties"]
properties["geometry"] = feature["geometry"]
Expand All @@ -309,6 +311,7 @@ def from_geojson(cls, feature: dict[str, Any]) -> EOProduct:
) from e
obj = cls(provider, properties, collection=collection)
obj.search_intersection = geometry.shape(search_intersection)
AssetsDict = _get_assets_dict_cls()
obj.assets = AssetsDict(obj, feature.get("assets", {}))
return obj

Expand All @@ -335,6 +338,8 @@ def _register_downloader_from_manager(self, plugins_manager: PluginManager) -> N
:param plugins_manager: The plugins manager instance to use for retrieving
the download and authentication plugins.
"""
from eodag.api.product.metadata_mapping import ONLINE_STATUS

download_plugin = plugins_manager.get_download_plugin(self)
if len(self.assets) > 0:
matching_url = next(iter(self.assets.values()))["href"]
Expand Down Expand Up @@ -527,6 +532,8 @@ def _download_quicklook(
HTTP request if the resource requires authentication.
:raises HTTPError: If the HTTP request to the quicklook URL fails.
"""
import requests

with requests.get(
self.properties["eodag:quicklook"],
stream=True,
Expand Down Expand Up @@ -639,6 +646,9 @@ def format_quicklook_address() -> None:
if self.downloader_auth is not None
else None
)
from requests import RequestException
from requests.auth import AuthBase # noqa: F811

if not isinstance(auth, AuthBase):
auth = None
# Read the ssl_verify parameter used on the provider config
Expand Down Expand Up @@ -675,8 +685,10 @@ def format_quicklook_address() -> None:

def get_driver(self) -> DatasetDriver:
"""Get the most appropriate driver"""
for driver_conf in DRIVERS:
from eodag.api.product.drivers import DRIVERS
from eodag.api.product.drivers.generic import GenericDriver

for driver_conf in DRIVERS:
# Select a driver if all criterias match
match = True
for criteria in driver_conf["criteria"]:
Expand All @@ -689,6 +701,8 @@ def get_driver(self) -> DatasetDriver:
return GenericDriver()

def _repr_html_(self):
from eodag.utils.repr import dict_to_html_table

thumbnail = self.properties.get("eodag:thumbnail") or self.properties.get(
"eodag:quicklook"
)
Expand Down
23 changes: 18 additions & 5 deletions eodag/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,8 @@
from urllib.parse import parse_qs

import click
from concurrent.futures import ThreadPoolExecutor

from eodag.api.collection import CollectionsList
from eodag.api.core import EODataAccessGateway, SearchResult
from eodag.utils import DEFAULT_LIMIT, DEFAULT_PAGE
from eodag.utils.exceptions import NoMatchingCollection, UnsupportedProvider
from eodag.utils.logging import setup_logging

if TYPE_CHECKING:
from click import Context
Expand Down Expand Up @@ -310,6 +305,9 @@ def search_crunch(ctx: Context, **kwargs: Any) -> None:
click.echo(search_crunch.get_help(ctx))
sys.exit(-1)

from eodag.api.core import EODataAccessGateway
from eodag.utils.logging import setup_logging

setup_logging(verbose=ctx.obj["verbosity"])

if kwargs["box"] != (None,) * 4:
Expand Down Expand Up @@ -442,6 +440,11 @@ def search_crunch(ctx: Context, **kwargs: Any) -> None:
@click.pass_context
def list_col(ctx: Context, **kwargs: Any) -> None:
"""Print the list of supported collections"""
from eodag.api.collection import CollectionsList
from eodag.api.core import EODataAccessGateway
from eodag.utils.exceptions import NoMatchingCollection, UnsupportedProvider
from eodag.utils.logging import setup_logging

setup_logging(verbose=ctx.obj["verbosity"])
dag = EODataAccessGateway()
provider = kwargs.pop("provider")
Expand Down Expand Up @@ -517,6 +520,9 @@ def list_col(ctx: Context, **kwargs: Any) -> None:
@click.pass_context
def discover_col(ctx: Context, **kwargs: Any) -> None:
"""Fetch external collections configuration and save result"""
from eodag.api.core import EODataAccessGateway
from eodag.utils.logging import setup_logging

setup_logging(verbose=ctx.obj["verbosity"])
dag = EODataAccessGateway()
provider = kwargs.pop("provider")
Expand Down Expand Up @@ -588,6 +594,13 @@ def download(ctx: Context, **kwargs: Any) -> None:
click.echo("Nothing to do (no search results file or stac item provided)")
click.echo(download.get_help(ctx))
sys.exit(1)

from concurrent.futures import ThreadPoolExecutor

from eodag.api.core import EODataAccessGateway
from eodag.api.search_result import SearchResult
from eodag.utils.logging import setup_logging

setup_logging(verbose=ctx.obj["verbosity"])
conf_file = kwargs.pop("conf")
if conf_file:
Expand Down
Loading
Loading