diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f2237dc7..b0e230bf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +- Speed up importing pystac ([#1584](https://github.com/stac-utils/pystac/pull/1584)) + ## [v1.14.1] - 2025-09-18 ### Fixed diff --git a/pystac/__init__.py b/pystac/__init__.py index 1fbbb8343..20be20f10 100644 --- a/pystac/__init__.py +++ b/pystac/__init__.py @@ -44,7 +44,6 @@ "set_stac_version", ] -import os import warnings from typing import Any @@ -199,6 +198,8 @@ def write_file( """ if stac_io is None: stac_io = StacIO.default() + import os + dest_href = None if dest_href is None else str(os.fspath(dest_href)) obj.save_object( include_self_link=include_self_link, dest_href=dest_href, stac_io=stac_io diff --git a/pystac/asset.py b/pystac/asset.py index 46d034bf7..32bbfadb8 100644 --- a/pystac/asset.py +++ b/pystac/asset.py @@ -1,13 +1,10 @@ from __future__ import annotations -import os import shutil from copy import copy, deepcopy -from html import escape from typing import TYPE_CHECKING, Any, Protocol, TypeVar from pystac import MediaType, STACError, common_metadata, utils -from pystac.html.jinja_env import get_jinja_env from pystac.utils import is_absolute_href, make_absolute_href, make_relative_href if TYPE_CHECKING: @@ -182,6 +179,10 @@ def __repr__(self) -> str: return f"" def _repr_html_(self) -> str: + from html import escape + + from pystac.html.jinja_env import get_jinja_env + jinja_env = get_jinja_env() if jinja_env: template = jinja_env.get_template("JSON.jinja2") @@ -259,6 +260,8 @@ def delete(self) -> None: Does not modify the asset. """ + import os + href = _absolute_href(self.href, self.owner, "delete") os.remove(href) diff --git a/pystac/catalog.py b/pystac/catalog.py index ae47787c0..aae2612f3 100644 --- a/pystac/catalog.py +++ b/pystac/catalog.py @@ -1,10 +1,8 @@ from __future__ import annotations import os -import warnings from collections.abc import Callable, Iterable, Iterator from copy import deepcopy -from itertools import chain from typing import ( TYPE_CHECKING, Any, @@ -521,6 +519,8 @@ def get_item(self, id: str, recursive: bool = False) -> Item | None: Return: Item or None: The item with the given ID, or None if not found. """ + import warnings + warnings.warn( "get_item is deprecated and will be removed in v2. " "Use next(self.get_items(id), None) instead", @@ -549,6 +549,8 @@ def get_items(self, *ids: str, recursive: bool = False) -> Iterator[Item]: (if recursive) all catalogs or collections connected to this catalog through child links. """ + from itertools import chain + items: Iterator[Item] if not recursive: items = map( @@ -615,6 +617,9 @@ def get_all_items(self) -> Iterator[Item]: catalogs or collections connected to this catalog through child links. """ + import warnings + from itertools import chain + warnings.warn( "get_all_items is deprecated and will be removed in v2", DeprecationWarning, diff --git a/pystac/collection.py b/pystac/collection.py index be8693d6c..536fd2830 100644 --- a/pystac/collection.py +++ b/pystac/collection.py @@ -1,6 +1,5 @@ from __future__ import annotations -import warnings from collections.abc import Iterable, Sequence from copy import deepcopy from datetime import datetime, timezone @@ -12,8 +11,6 @@ cast, ) -from dateutil import tz - import pystac from pystac import CatalogType, STACObjectType from pystac.asset import Asset, Assets @@ -257,6 +254,8 @@ def from_dict(d: dict[str, Any]) -> TemporalExtent: parsed_intervals: list[list[datetime | None]] = [] for i in d["interval"]: if isinstance(i, str): + import warnings + # d["interval"] is a list of strings, so we correct the list and # try again # https://github.com/stac-utils/pystac/issues/1221 @@ -384,6 +383,8 @@ def from_items( Extent: An Extent that spatially and temporally covers all of the given items. """ + from dateutil import tz + bounds_values: list[list[float]] = [ [float("inf")], [float("inf")], @@ -635,6 +636,8 @@ def from_dict( migrate: bool = True, preserve_dict: bool = True, ) -> C: + import warnings + from pystac.extensions.version import CollectionVersionExtension if migrate: diff --git a/pystac/extensions/eo.py b/pystac/extensions/eo.py index 4de7a0268..1f4b12249 100644 --- a/pystac/extensions/eo.py +++ b/pystac/extensions/eo.py @@ -2,7 +2,6 @@ from __future__ import annotations -import warnings from collections.abc import Iterable from typing import ( Any, @@ -386,6 +385,8 @@ def get_schema_uri(cls) -> str: @classmethod def get_schema_uris(cls) -> list[str]: + import warnings + warnings.warn( "get_schema_uris is deprecated and will be removed in v2", DeprecationWarning, diff --git a/pystac/extensions/file.py b/pystac/extensions/file.py index 61349ef83..b772123bf 100644 --- a/pystac/extensions/file.py +++ b/pystac/extensions/file.py @@ -2,7 +2,6 @@ from __future__ import annotations -import warnings from collections.abc import Iterable from typing import Any, Generic, Literal, TypeVar, cast @@ -370,6 +369,8 @@ def migrate( found_fields[asset_key] = values if found_fields: + import warnings + warnings.warn( f"Assets {list(found_fields.keys())} contain fields: " f"{list(set.union(*found_fields.values()))} which " diff --git a/pystac/extensions/grid.py b/pystac/extensions/grid.py index 112fe75cb..39bca99b1 100644 --- a/pystac/extensions/grid.py +++ b/pystac/extensions/grid.py @@ -3,7 +3,6 @@ from __future__ import annotations import re -import warnings from re import Pattern from typing import Any, Literal @@ -92,6 +91,8 @@ def get_schema_uri(cls) -> str: @classmethod def get_schema_uris(cls) -> list[str]: + import warnings + warnings.warn( "get_schema_uris is deprecated and will be removed in v2", DeprecationWarning, diff --git a/pystac/extensions/item_assets.py b/pystac/extensions/item_assets.py index 140207275..1743deed6 100644 --- a/pystac/extensions/item_assets.py +++ b/pystac/extensions/item_assets.py @@ -2,7 +2,6 @@ from __future__ import annotations -import warnings from typing import Any, Literal import pystac @@ -27,6 +26,8 @@ class AssetDefinition(ItemAssetDefinition): """ def __init__(cls, *args: Any, **kwargs: Any) -> None: + import warnings + warnings.warn( ( "``AssetDefinition`` is deprecated. " @@ -49,6 +50,8 @@ class ItemAssetsExtension(ExtensionManagementMixin[pystac.Collection]): collection: pystac.Collection def __init__(self, collection: pystac.Collection) -> None: + import warnings + warnings.warn( ( "The ``item_assets`` extension is deprecated. " diff --git a/pystac/extensions/projection.py b/pystac/extensions/projection.py index 4adf3dae2..a0979c995 100644 --- a/pystac/extensions/projection.py +++ b/pystac/extensions/projection.py @@ -2,8 +2,6 @@ from __future__ import annotations -import json -import warnings from collections.abc import Iterable from typing import ( Any, @@ -223,6 +221,8 @@ def crs_string(self) -> str | None: elif self.wkt2: return self.wkt2 elif self.projjson: + import json + return json.dumps(self.projjson) else: return None @@ -321,6 +321,8 @@ def get_schema_uri(cls) -> str: @classmethod def get_schema_uris(cls) -> list[str]: + import warnings + warnings.warn( "get_schema_uris is deprecated and will be removed in v2", DeprecationWarning, diff --git a/pystac/extensions/raster.py b/pystac/extensions/raster.py index ed239466f..0f59b45b6 100644 --- a/pystac/extensions/raster.py +++ b/pystac/extensions/raster.py @@ -2,7 +2,6 @@ from __future__ import annotations -import warnings from collections.abc import Iterable from typing import ( Any, @@ -716,6 +715,8 @@ def get_schema_uri(cls) -> str: @classmethod def get_schema_uris(cls) -> list[str]: + import warnings + warnings.warn( "get_schema_uris is deprecated and will be removed in v2", DeprecationWarning, diff --git a/pystac/extensions/scientific.py b/pystac/extensions/scientific.py index b0d7821e7..9f6316b70 100644 --- a/pystac/extensions/scientific.py +++ b/pystac/extensions/scientific.py @@ -7,9 +7,7 @@ from __future__ import annotations -import copy from typing import Any, Generic, Literal, TypeVar, cast -from urllib import parse import pystac from pystac.extensions.base import ( @@ -49,6 +47,8 @@ class ScientificRelType(StringEnum): def doi_to_url(doi: str) -> str: """Converts a DOI to the corresponding URL.""" + from urllib import parse + return DOI_URL_BASE + parse.quote(doi) @@ -72,6 +72,8 @@ def __repr__(self) -> str: return f"" def to_dict(self) -> dict[str, str | None]: + import copy + return copy.deepcopy({"doi": self.doi, "citation": self.citation}) @staticmethod diff --git a/pystac/extensions/version.py b/pystac/extensions/version.py index b74d3f4c9..b1ad6016d 100644 --- a/pystac/extensions/version.py +++ b/pystac/extensions/version.py @@ -2,7 +2,6 @@ from __future__ import annotations -import warnings from collections.abc import Generator from contextlib import contextmanager from typing import ( @@ -437,6 +436,8 @@ def ignore_deprecated() -> Generator[None]: """Context manager for suppressing the :class:`pystac.DeprecatedWarning` when creating a deprecated :class:`~pystac.Item` or :class:`~pystac.Collection` from a dictionary.""" + import warnings + with warnings.catch_warnings(): warnings.simplefilter("ignore", category=DeprecatedWarning) yield diff --git a/pystac/item.py b/pystac/item.py index f95c5a218..6a8bf4f53 100644 --- a/pystac/item.py +++ b/pystac/item.py @@ -1,6 +1,5 @@ from __future__ import annotations -import warnings from copy import copy, deepcopy from typing import TYPE_CHECKING, Any, TypeVar, cast @@ -419,6 +418,8 @@ def from_dict( migrate: bool = True, preserve_dict: bool = True, ) -> T: + import warnings + from pystac.extensions.version import ItemVersionExtension if preserve_dict: diff --git a/pystac/item_assets.py b/pystac/item_assets.py index 2260cdf0b..ab3a1fadb 100644 --- a/pystac/item_assets.py +++ b/pystac/item_assets.py @@ -6,7 +6,6 @@ from __future__ import annotations -from copy import deepcopy from typing import TYPE_CHECKING, Any import pystac @@ -179,6 +178,8 @@ def roles(self, v: list[str] | None) -> None: def to_dict(self) -> dict[str, Any]: """Returns a dictionary representing this ``ItemAssetDefinition``.""" + from copy import deepcopy + return deepcopy(self.properties) def create_asset(self, href: str) -> pystac.Asset: diff --git a/pystac/item_collection.py b/pystac/item_collection.py index 5bf82c649..30634157d 100644 --- a/pystac/item_collection.py +++ b/pystac/item_collection.py @@ -1,8 +1,6 @@ from __future__ import annotations from collections.abc import Collection, Iterable, Iterator -from copy import deepcopy -from html import escape from typing import ( Any, TypeAlias, @@ -11,7 +9,6 @@ import pystac from pystac.errors import STACTypeError -from pystac.html.jinja_env import get_jinja_env from pystac.serialization.identify import identify_stac_object_type from pystac.utils import HREF, is_absolute_href, make_absolute_href, make_posix_style @@ -147,6 +144,10 @@ def to_dict(self, transform_hrefs: bool = False) -> dict[str, Any]: } def _repr_html_(self) -> str: + from html import escape + + from pystac.html.jinja_env import get_jinja_env + jinja_env = get_jinja_env() if jinja_env: template = jinja_env.get_template("JSON.jinja2") @@ -158,6 +159,8 @@ def clone(self) -> ItemCollection: """Creates a clone of this instance. This clone is a deep copy; all :class:`~pystac.Item` instances are cloned and all additional top-level fields are deep copied.""" + from copy import deepcopy + return self.__class__( items=[item.clone() for item in self.items], extra_fields=deepcopy(self.extra_fields), diff --git a/pystac/layout.py b/pystac/layout.py index 88c963d12..b36668488 100644 --- a/pystac/layout.py +++ b/pystac/layout.py @@ -1,12 +1,9 @@ from __future__ import annotations -import os import posixpath -import warnings from abc import ABC, abstractmethod from collections import OrderedDict from collections.abc import Callable -from string import Formatter from typing import TYPE_CHECKING, Any import pystac @@ -30,6 +27,8 @@ class TemplateError(Exception): """ def __init__(self, *args, **kwargs) -> None: # type: ignore + import warnings + warnings.warn( message=( "TemplateError in pystac.layout is deprecated and will be " @@ -109,6 +108,8 @@ class LayoutTemplate: ITEM_TEMPLATE_VARS = ["date", "year", "month", "day", "collection"] def __init__(self, template: str, defaults: dict[str, str] | None = None) -> None: + from string import Formatter + self.template = template self.defaults = defaults or {} @@ -261,6 +262,8 @@ class HrefLayoutStrategy(ABC): def get_href( self, stac_object: STACObject, parent_dir: str, is_root: bool = False ) -> str: + import os + if is_file_path(parent_dir): parent_dir = os.path.dirname(parent_dir) diff --git a/pystac/link.py b/pystac/link.py index 7b1cc4a0d..797f053e4 100644 --- a/pystac/link.py +++ b/pystac/link.py @@ -1,13 +1,10 @@ from __future__ import annotations import os -from copy import copy -from html import escape from typing import TYPE_CHECKING, Any, TypeVar import pystac from pystac.errors import STACError -from pystac.html.jinja_env import get_jinja_env from pystac.utils import ( HREF as HREF, ) @@ -272,6 +269,10 @@ def __repr__(self) -> str: return f"" def _repr_html_(self) -> str: + from html import escape + + from pystac.html.jinja_env import get_jinja_env + jinja_env = get_jinja_env() if jinja_env: template = jinja_env.get_template("JSON.jinja2") @@ -435,6 +436,8 @@ def from_dict(cls: type[L], d: dict[str, Any]) -> L: Returns: Link: Link instance constructed from the dict. """ + from copy import copy + d = copy(d) rel = d.pop("rel") href = d.pop("href") diff --git a/pystac/provider.py b/pystac/provider.py index 4152a28be..4db1f9174 100644 --- a/pystac/provider.py +++ b/pystac/provider.py @@ -1,7 +1,5 @@ -from html import escape from typing import Any -from pystac.html.jinja_env import get_jinja_env from pystac.utils import StringEnum @@ -73,6 +71,10 @@ def __eq__(self, o: object) -> bool: return self.to_dict() == o.to_dict() def _repr_html_(self) -> str: + from html import escape + + from pystac.html.jinja_env import get_jinja_env + jinja_env = get_jinja_env() if jinja_env: template = jinja_env.get_template("JSON.jinja2") diff --git a/pystac/serialization/migrate.py b/pystac/serialization/migrate.py index 8b1871bd2..4ed267437 100644 --- a/pystac/serialization/migrate.py +++ b/pystac/serialization/migrate.py @@ -1,7 +1,6 @@ from __future__ import annotations from collections.abc import Callable -from copy import deepcopy from typing import TYPE_CHECKING, Any import pystac @@ -167,6 +166,8 @@ def migrate_to_latest( dict: A copy of the dict that is migrated to the latest version (the version that is pystac.version.STACVersion.DEFAULT_STAC_VERSION) """ + from copy import deepcopy + result = deepcopy(json_dict) version = info.version_range.latest_valid_version() diff --git a/pystac/stac_io.py b/pystac/stac_io.py index d194a8283..1436d7ffc 100644 --- a/pystac/stac_io.py +++ b/pystac/stac_io.py @@ -1,13 +1,10 @@ from __future__ import annotations import json -import logging import os from abc import ABC, abstractmethod from collections.abc import Callable from typing import TYPE_CHECKING, Any -from urllib.error import HTTPError -from urllib.request import Request, urlopen import pystac from pystac.serialization import ( @@ -37,9 +34,6 @@ from pystac.stac_object import STACObject -logger = logging.getLogger(__name__) - - class StacIO(ABC): _default_io: Callable[[], StacIO] | None = None @@ -296,6 +290,11 @@ def read_text_from_href(self, href: str) -> str: """ href_contents: str if _is_url(href): + import logging + from urllib.error import HTTPError + from urllib.request import Request, urlopen + + logger = logging.getLogger(__name__) try: logger.debug(f"GET {href} Headers: {self.headers}") if HAS_URLLIB3: @@ -451,6 +450,8 @@ def read_text_from_href(self, href: str) -> str: href : The URI of the file to open. """ if _is_url(href): + from urllib.error import HTTPError + # TODO provide a pooled StacIO to enable more efficient network # access (probably named `PooledStacIO`). http = PoolManager() diff --git a/pystac/stac_object.py b/pystac/stac_object.py index c95c83948..81545faf7 100644 --- a/pystac/stac_object.py +++ b/pystac/stac_object.py @@ -2,12 +2,10 @@ from abc import ABC, abstractmethod from collections.abc import Callable, Iterable -from html import escape from typing import TYPE_CHECKING, Any, TypeAlias, TypeVar, cast import pystac from pystac import STACError -from pystac.html.jinja_env import get_jinja_env from pystac.link import Link from pystac.utils import ( HREF, @@ -584,6 +582,10 @@ def to_dict( raise NotImplementedError def _repr_html_(self) -> str: + from html import escape + + from pystac.html.jinja_env import get_jinja_env + jinja_env = get_jinja_env() if jinja_env: template = jinja_env.get_template("JSON.jinja2") diff --git a/pystac/summaries.py b/pystac/summaries.py index 1e3998008..c3b9715bf 100644 --- a/pystac/summaries.py +++ b/pystac/summaries.py @@ -1,11 +1,7 @@ from __future__ import annotations -import importlib.resources -import json -import numbers from abc import abstractmethod from collections.abc import Iterable -from copy import deepcopy from enum import Enum from functools import lru_cache from typing import ( @@ -100,6 +96,9 @@ def __repr__(self) -> str: @lru_cache(maxsize=None) def _get_fields_json(url: str | None) -> dict[str, Any]: if url is None: + import importlib.resources + import json + # Every time pystac is released this file gets pulled from # https://cdn.jsdelivr.net/npm/@radiantearth/stac-fields/fields-normalized.json jsonfields: dict[str, Any] = json.loads( @@ -170,6 +169,8 @@ def _set_field_definitions(self, fields: dict[str, Any]) -> None: self.summaryfields[name] = strategy def _update_with_item(self, summaries: Summaries, item: Item) -> None: + import numbers + for k, v in item.properties.items(): if k in self.summaryfields: strategy = self.summaryfields[k] @@ -310,6 +311,8 @@ def clone(self) -> Summaries: Returns: Summaries: The clone of this object """ + from copy import deepcopy + cls = self.__class__ summaries = cls(summaries=deepcopy(self._summaries), maxcount=self.maxcount) summaries.lists = deepcopy(self.lists) diff --git a/pystac/utils.py b/pystac/utils.py index 4225fd1d5..623ea4889 100644 --- a/pystac/utils.py +++ b/pystac/utils.py @@ -2,7 +2,6 @@ import os import posixpath -import warnings from collections.abc import Callable from datetime import datetime, timezone from enum import Enum @@ -15,8 +14,6 @@ from urllib.parse import ParseResult as URLParseResult from urllib.parse import urljoin, urlparse, urlunparse -import dateutil.parser - from pystac.errors import RequiredPropertyMissing #: HREF string or path-like object. @@ -132,6 +129,8 @@ def from_parsed_uri(parsed_uri: URLParseResult) -> JoinType: Returns: JoinType : The join type for the URI. """ + import warnings + warnings.warn( message=( "from_parsed_uri is deprecated and will be removed in pystac " @@ -166,6 +165,8 @@ def join_path_or_url(join_type: JoinType, *args: str) -> str: Returns: str : The joined path """ + import warnings + warnings.warn( message=( "join_path_or_url is deprecated and will be removed in pystac " @@ -432,6 +433,8 @@ def str_to_datetime(s: str) -> datetime: Returns: str: The :class:`datetime.datetime` represented the by the string. """ + import dateutil.parser + return dateutil.parser.isoparse(s) diff --git a/pystac/validation/__init__.py b/pystac/validation/__init__.py index 4b9be8269..f48629c62 100644 --- a/pystac/validation/__init__.py +++ b/pystac/validation/__init__.py @@ -1,6 +1,5 @@ from __future__ import annotations -import warnings from collections.abc import Iterable, Mapping from typing import TYPE_CHECKING, Any, cast @@ -164,6 +163,8 @@ def validate_all( stac_io = pystac.StacIO.default() if isinstance(stac_object, dict): + import warnings + warnings.warn( "validating a STAC object as a dict is deprecated;" " use validate_all_dict instead", diff --git a/pystac/validation/local_validator.py b/pystac/validation/local_validator.py index 88f2a7447..93a7e40aa 100644 --- a/pystac/validation/local_validator.py +++ b/pystac/validation/local_validator.py @@ -1,10 +1,7 @@ -import importlib.resources import json -import warnings from typing import Any, cast from jsonschema import Draft7Validator, ValidationError -from referencing import Registry, Resource from pystac.errors import STACLocalValidationError from pystac.version import STACVersion @@ -13,6 +10,8 @@ def _read_schema(file_name: str) -> dict[str, Any]: + import importlib.resources + with ( importlib.resources.files("pystac.validation.jsonschemas") .joinpath(file_name) @@ -73,6 +72,8 @@ def get_local_schema_cache() -> dict[str, dict[str, Any]]: def __getattr__(name: str) -> Any: if name in deprecated_names: + import warnings + warnings.warn(f"{name} is deprecated and will be removed in v2.", FutureWarning) return globals()[f"_deprecated_{name}"] raise AttributeError(f"module {__name__} has no attribute {name}") @@ -81,6 +82,8 @@ def __getattr__(name: str) -> Any: class LocalValidator: def __init__(self) -> None: """DEPRECATED""" + import warnings + warnings.warn( "``LocalValidator`` is deprecated and will be removed in v2.", DeprecationWarning, @@ -88,6 +91,8 @@ def __init__(self) -> None: self.schema_cache = get_local_schema_cache() def registry(self) -> Any: + from referencing import Registry, Resource + return Registry().with_resources( [(k, Resource.from_contents(v)) for k, v in self.schema_cache.items()] ) diff --git a/pystac/version.py b/pystac/version.py index 2e5161947..1c54dcd94 100644 --- a/pystac/version.py +++ b/pystac/version.py @@ -1,5 +1,3 @@ -import os - __version__ = "1.14.1" """Library version""" @@ -21,6 +19,8 @@ def get_stac_version(cls) -> str: if cls._override_version is not None: return cls._override_version + import os + env_version = os.environ.get(cls.OVERRIDE_VERSION_ENV_VAR) if env_version is not None: return env_version