<gh_stars>0
from enum import Enum
from functools import total_ordering
from typing import Any, Dict, List, Optional, Set, TYPE_CHECKING, Union, cast

import pystac
from pystac.version import STACVersion

if TYPE_CHECKING:
    from pystac.stac_object import STACObjectType as STACObjectType_Type


class OldExtensionShortIDs(Enum):
    """Enumerates the IDs of common extensions."""

    CHECKSUM = "checksum"
    COLLECTION_ASSETS = "collection-assets"
    DATACUBE = "datacube"  # TODO
    EO = "eo"
    ITEM_ASSETS = "item-assets"  # TODO
    LABEL = "label"
    POINTCLOUD = "pointcloud"
    PROJECTION = "projection"
    SAR = "sar"
    SAT = "sat"
    SCIENTIFIC = "scientific"
    SINGLE_FILE_STAC = "single-file-stac"
    TILED_ASSETS = "tiled-assets"
    TIMESTAMPS = "timestamps"
    VERSION = "version"
    VIEW = "view"
    FILE = "file"


@total_ordering
class STACVersionID:
    """Defines STAC versions in an object that is orderable based on version number.
    For instance, ``1.0.0-beta.2 < 1.0.0``
    """

    def __init__(self, version_string: str) -> None:
        self.version_string = version_string

        # Account for RC or beta releases in version
        version_parts = version_string.split("-")
        self.version_core = version_parts[0]
        if len(version_parts) == 1:
            self.version_prerelease = None
        else:
            self.version_prerelease = "-".join(version_parts[1:])

    def __str__(self) -> str:
        return self.version_string

    def __eq__(self, other: Any) -> bool:
        if not isinstance(other, STACVersionID):
            other = STACVersionID(str(other))
        return str(self) == str(other)

    def __ne__(self, other: object) -> bool:
        return not self.__eq__(other)

    def __lt__(self, other: object) -> bool:
        if not isinstance(other, STACVersionID):
            other = STACVersionID(str(other))
        if self.version_core < other.version_core:
            return True
        elif self.version_core > other.version_core:
            return False
        else:
            return self.version_prerelease is not None and (
                other.version_prerelease is None
                or other.version_prerelease > self.version_prerelease
            )


class STACVersionRange:
    """Defines a range of STAC versions."""

    def __init__(
        self,
        min_version: Union[str, STACVersionID] = "0.4.0",
        max_version: Optional[Union[str, STACVersionID]] = None,
    ):
        if isinstance(min_version, str):
            self.min_version = STACVersionID(min_version)
        else:
            self.min_version = min_version

        if max_version is None:
            self.max_version = STACVersionID(STACVersion.DEFAULT_STAC_VERSION)
        else:
            if isinstance(max_version, str):
                self.max_version = STACVersionID(max_version)
            else:
                self.max_version = max_version

    def set_min(self, v: STACVersionID) -> None:
        if self.min_version < v:
            if v < self.max_version:
                self.min_version = v
            else:
                self.min_version = self.max_version

    def set_max(self, v: STACVersionID) -> None:
        if v < self.max_version:
            if self.min_version < v:
                self.max_version = v
            else:
                self.max_version = self.min_version

    def set_to_single(self, v: STACVersionID) -> None:
        self.set_min(v)
        self.set_max(v)

    def latest_valid_version(self) -> STACVersionID:
        return self.max_version

    def contains(self, v: Union[str, STACVersionID]) -> bool:
        if isinstance(v, str):
            v = STACVersionID(v)
        return self.min_version <= v and v <= self.max_version  # type:ignore

    def is_single_version(self) -> bool:
        return self.min_version >= self.max_version  # type:ignore

    def is_earlier_than(self, v: Union[str, STACVersionID]) -> bool:
        if isinstance(v, str):
            v = STACVersionID(v)
        return self.max_version < v

    def is_later_than(self, v: Union[str, STACVersionID]) -> bool:
        if isinstance(v, str):
            v = STACVersionID(v)
        return v < self.min_version

    def __repr__(self) -> str:
        return "<VERSIONS {}-{}>".format(self.min_version, self.max_version)


class STACJSONDescription:
    """Describes the STAC object information for a STAC object represented in JSON

    Attributes:
        object_type : Describes the STAC object type. One of
            :class:`~pystac.STACObjectType`.
        version_range : The STAC version range that describes what
            has been identified as potential valid versions of the stac object.
        extensions : List of extension schema URIs for extensions this
            object implements
    """

    def __init__(
        self,
        object_type: "STACObjectType_Type",
        version_range: STACVersionRange,
        extensions: Set[str],
    ) -> None:
        self.object_type = object_type
        self.version_range = version_range
        self.extensions = extensions

    def __repr__(self) -> str:
        return "<{} {} ext={}>".format(
            self.object_type, self.version_range, ",".join(self.extensions)
        )


def _identify_stac_extensions(
    object_type: str, d: Dict[str, Any], version_range: STACVersionRange
) -> List[str]:
    """Identifies extensions for STAC Objects that don't list their
    extensions in a 'stac_extensions' property.

    Returns a list of stac_extensions. May mutate the version_range to update
    min or max version.
    """
    stac_extensions: Set[str] = set([])

    # assets (collection assets)

    if object_type == pystac.STACObjectType.ITEMCOLLECTION:
        if "assets" in d:
            stac_extensions.add("assets")
            version_range.set_min(STACVersionID("0.8.0"))

    # checksum
    if "links" in d:
        found_checksum = False
        for link in d["links"]:
            # Account for old links as dicts
            if isinstance(link, str):
                link_props = cast(Dict[str, Any], d["links"][link]).keys()
            else:
                link_props = cast(Dict[str, Any], link).keys()

            if any(prop.startswith("checksum:") for prop in link_props):
                found_checksum = True
                stac_extensions.add(OldExtensionShortIDs.CHECKSUM.value)
        if not found_checksum:
            if "assets" in d:
                for asset in d["assets"].values():
                    asset_props = cast(Dict[str, Any], asset).keys()
                    if any(prop.startswith("checksum:") for prop in asset_props):
                        found_checksum = True
                        stac_extensions.add(OldExtensionShortIDs.CHECKSUM.value)
        if found_checksum:
            version_range.set_min(STACVersionID("0.6.2"))

    # datacube
    if object_type == pystac.STACObjectType.ITEM:
        if any(k.startswith("cube:") for k in cast(Dict[str, Any], d["properties"])):
            stac_extensions.add(OldExtensionShortIDs.DATACUBE.value)
            version_range.set_min(STACVersionID("0.6.1"))

    # datetime-range (old extension)
    if object_type == pystac.STACObjectType.ITEM:
        if "dtr:start_datetime" in d["properties"]:
            stac_extensions.add("datetime-range")
            version_range.set_min(STACVersionID("0.6.0"))

    # eo
    if object_type == pystac.STACObjectType.ITEM:
        if any(k.startswith("eo:") for k in cast(Dict[str, Any], d["properties"])):
            stac_extensions.add(OldExtensionShortIDs.EO.value)
            if "eo:epsg" in d["properties"]:
                if d["properties"]["eo:epsg"] is None:
                    version_range.set_min(STACVersionID("0.6.1"))
            if "eo:crs" in d["properties"]:
                version_range.set_max(STACVersionID("0.4.1"))
            if "eo:constellation" in d["properties"]:
                version_range.set_min(STACVersionID("0.6.0"))
        if "eo:bands" in d:
            stac_extensions.add(OldExtensionShortIDs.EO.value)
            version_range.set_max(STACVersionID("0.5.2"))

    # pointcloud
    if object_type == pystac.STACObjectType.ITEM:
        if any(k.startswith("pc:") for k in cast(Dict[str, Any], d["properties"])):
            stac_extensions.add(OldExtensionShortIDs.POINTCLOUD.value)
            version_range.set_min(STACVersionID("0.6.2"))

    # sar
    if object_type == pystac.STACObjectType.ITEM:
        if any(k.startswith("sar:") for k in cast(Dict[str, Any], d["properties"])):
            stac_extensions.add(OldExtensionShortIDs.SAR.value)
            version_range.set_min(STACVersionID("0.6.2"))
            if version_range.contains("0.6.2"):
                for prop in [
                    "sar:absolute_orbit",
                    "sar:resolution",
                    "sar:pixel_spacing",
                    "sar:looks",
                ]:
                    if prop in d["properties"]:
                        if isinstance(d["properties"][prop], list):
                            version_range.set_max(STACVersionID("0.6.2"))
            if version_range.contains("0.7.0"):
                for prop in [
                    "sar:incidence_angle",
                    "sar:relative_orbit",
                    "sar:observation_direction",
                    "sar:resolution_range",
                    "sar:resolution_azimuth",
                    "sar:pixel_spacing_range",
                    "sar:pixel_spacing_azimuth",
                    "sar:looks_range",
                    "sar:looks_azimuth",
                    "sar:looks_equivalent_number",
                ]:
                    if prop in d["properties"]:
                        version_range.set_min(STACVersionID("0.7.0"))
                if "sar:absolute_orbit" in d["properties"] and not isinstance(
                    d["properties"]["sar:absolute_orbit"], list
                ):
                    version_range.set_min(STACVersionID("0.7.0"))
            if "sar:off_nadir" in d["properties"]:
                version_range.set_max(STACVersionID("0.6.2"))

    # scientific
    if (
        object_type == pystac.STACObjectType.ITEM
        or object_type == pystac.STACObjectType.COLLECTION
    ):
        if "properties" in d:
            prop_keys = cast(Dict[str, Any], d["properties"]).keys()
            if any(k.startswith("sci:") for k in prop_keys):
                stac_extensions.add(OldExtensionShortIDs.SCIENTIFIC.value)
                version_range.set_min(STACVersionID("0.6.0"))

    # Single File STAC
    if object_type == pystac.STACObjectType.ITEMCOLLECTION:
        if "collections" in d:
            stac_extensions.add(OldExtensionShortIDs.SINGLE_FILE_STAC.value)
            version_range.set_min(STACVersionID("0.8.0"))
            if "stac_extensions" not in d:
                version_range.set_max(STACVersionID("0.8.1"))

    return list(stac_extensions)


def identify_stac_object_type(json_dict: Dict[str, Any]) -> "STACObjectType_Type":
    """Determines the STACObjectType of the provided JSON dict.

    Args:
        json_dict : The dict of STAC JSON to identify.

    Returns:
        STACObjectType: The object type represented by the JSON.
    """
    object_type = None

    if "type" in json_dict:  # Try to identify using 'type' property
        for t in pystac.STACObjectType:
            if json_dict["type"].lower() == t.value.lower():
                object_type = t
                break

    if object_type is None:  # Use old-approach based on other properties
        # Identify pre-1.0 ITEMCOLLECTION (since removed)
        if "type" in json_dict and "assets" not in json_dict:
            if "stac_version" in json_dict and json_dict["stac_version"].startswith(
                "0"
            ):
                if json_dict["type"] == "FeatureCollection":
                    object_type = pystac.STACObjectType.ITEMCOLLECTION

        if "extent" in json_dict:
            object_type = pystac.STACObjectType.COLLECTION
        elif "assets" in json_dict:
            object_type = pystac.STACObjectType.ITEM
        else:
            object_type = pystac.STACObjectType.CATALOG

    return object_type


def identify_stac_object(json_dict: Dict[str, Any]) -> STACJSONDescription:
    """Determines the STACJSONDescription of the provided JSON dict.

    Args:
        json_dict : The dict of STAC JSON to identify.

    Returns:
        STACJSONDescription: The description of the STAC object serialized in the
        given dict.
    """
    object_type = identify_stac_object_type(json_dict)

    version_range = STACVersionRange()

    stac_version = json_dict.get("stac_version")
    stac_extensions = json_dict.get("stac_extensions", None)

    if stac_version is None:
        if (
            object_type == pystac.STACObjectType.CATALOG
            or object_type == pystac.STACObjectType.COLLECTION
        ):
            version_range.set_max(STACVersionID("0.5.2"))
        elif object_type == pystac.STACObjectType.ITEM:
            version_range.set_max(STACVersionID("0.7.0"))
        else:  # ItemCollection
            version_range.set_min(STACVersionID("0.8.0"))
    else:
        version_range.set_to_single(stac_version)

    if stac_extensions is not None:
        version_range.set_min(STACVersionID("0.8.0"))

    if stac_extensions is None:
        # If this is post-0.8, we can assume there are no extensions
        # if the stac_extensions property doesn't exist for everything
        # but ItemCollection (except after 0.9.0, when ItemCollection also got
        # the stac_extensions property).
        if version_range.is_earlier_than("0.8.0") or (
            object_type == pystac.STACObjectType.ITEMCOLLECTION
            and not version_range.is_later_than("0.8.1")
        ):
            stac_extensions = _identify_stac_extensions(
                object_type, json_dict, version_range
            )
        else:
            stac_extensions = []

        # Between 1.0.0-beta.2 and 1.0.0-RC1, STAC extensions changed from
        # being split between 'common' and custom extensions, with common
        # extensions having short names that were used in the stac_extensions
        # property list, to being mostly externalized from the core spec and
        # always identified with the schema URI as the identifier. This
        # code translates the short name IDs used pre-1.0.0-RC1 to the
        # relevant extension schema uri identifier.

    if not version_range.is_single_version():
        # Final Checks

        if "links" in json_dict:
            # links were a dictionary only in 0.5
            if "links" in json_dict and isinstance(json_dict["links"], dict):
                version_range.set_to_single(STACVersionID("0.5.2"))

            # self links became non-required in 0.7.0
            if not version_range.is_earlier_than("0.7.0") and not any(
                filter(
                    lambda l: cast(Dict[str, Any], l)["rel"] == pystac.RelType.SELF,
                    json_dict["links"],
                )
            ):
                version_range.set_min(STACVersionID("0.7.0"))

    return STACJSONDescription(object_type, version_range, set(stac_extensions))

from enum import Enum
from functools import total_ordering
from typing import Any, Dict, List, Optional, Set, TYPE_CHECKING, Union, cast

import pystac
from pystac.version import STACVersion

if TYPE_CHECKING:
    from pystac.stac_object import STACObjectType as STACObjectType_Type


class OldExtensionShortIDs(Enum):
    """Enumerates the IDs of common extensions."""

    CHECKSUM = "checksum"
    COLLECTION_ASSETS = "collection-assets"
    DATACUBE = "datacube"  # TODO
    EO = "eo"
    ITEM_ASSETS = "item-assets"  # TODO
    LABEL = "label"
    POINTCLOUD = "pointcloud"
    PROJECTION = "projection"
    SAR = "sar"
    SAT = "sat"
    SCIENTIFIC = "scientific"
    SINGLE_FILE_STAC = "single-file-stac"
    TILED_ASSETS = "tiled-assets"
    TIMESTAMPS = "timestamps"
    VERSION = "version"
    VIEW = "view"
    FILE = "file"


@total_ordering
class STACVersionID:
    """Defines STAC versions in an object that is orderable based on version number.
    For instance, ``1.0.0-beta.2 < 1.0.0``
    """

    def __init__(self, version_string: str) -> None:
        self.version_string = version_string

        # Account for RC or beta releases in version
        version_parts = version_string.split("-")
        self.version_core = version_parts[0]
        if len(version_parts) == 1:
            self.version_prerelease = None
        else:
            self.version_prerelease = "-".join(version_parts[1:])

    def __str__(self) -> str:
        return self.version_string

    def __eq__(self, other: Any) -> bool:
        if not isinstance(other, STACVersionID):
            other = STACVersionID(str(other))
        return str(self) == str(other)

    def __ne__(self, other: object) -> bool:
        return not self.__eq__(other)

    def __lt__(self, other: object) -> bool:
        if not isinstance(other, STACVersionID):
            other = STACVersionID(str(other))
        if self.version_core < other.version_core:
            return True
        elif self.version_core > other.version_core:
            return False
        else:
            return self.version_prerelease is not None and (
                other.version_prerelease is None
                or other.version_prerelease > self.version_prerelease
            )


class STACVersionRange:
    """Defines a range of STAC versions."""

    def __init__(
        self,
        min_version: Union[str, STACVersionID] = "0.4.0",
        max_version: Optional[Union[str, STACVersionID]] = None,
    ):
        if isinstance(min_version, str):
            self.min_version = STACVersionID(min_version)
        else:
            self.min_version = min_version

        if max_version is None:
            self.max_version = STACVersionID(STACVersion.DEFAULT_STAC_VERSION)
        else:
            if isinstance(max_version, str):
                self.max_version = STACVersionID(max_version)
            else:
                self.max_version = max_version

    def set_min(self, v: STACVersionID) -> None:
        if self.min_version < v:
            if v < self.max_version:
                self.min_version = v
            else:
                self.min_version = self.max_version

    def set_max(self, v: STACVersionID) -> None:
        if v < self.max_version:
            if self.min_version < v:
                self.max_version = v
            else:
                self.max_version = self.min_version

    def set_to_single(self, v: STACVersionID) -> None:
        self.set_min(v)
        self.set_max(v)

    def latest_valid_version(self) -> STACVersionID:
        return self.max_version

    def contains(self, v: Union[str, STACVersionID]) -> bool:
        if isinstance(v, str):
            v = STACVersionID(v)
        return self.min_version <= v and v <= self.max_version  # type:ignore

    def is_single_version(self) -> bool:
        return self.min_version >= self.max_version  # type:ignore

    def is_earlier_than(self, v: Union[str, STACVersionID]) -> bool:
        if isinstance(v, str):
            v = STACVersionID(v)
        return self.max_version < v

    def is_later_than(self, v: Union[str, STACVersionID]) -> bool:
        if isinstance(v, str):
            v = STACVersionID(v)
        return v < self.min_version

    def __repr__(self) -> str:
        return "<VERSIONS {}-{}>".format(self.min_version, self.max_version)


class STACJSONDescription:
    """Describes the STAC object information for a STAC object represented in JSON

    Attributes:
        object_type : Describes the STAC object type. One of
            :class:`~pystac.STACObjectType`.
        version_range : The STAC version range that describes what
            has been identified as potential valid versions of the stac object.
        extensions : List of extension schema URIs for extensions this
            object implements
    """

    def __init__(
        self,
        object_type: "STACObjectType_Type",
        version_range: STACVersionRange,
        extensions: Set[str],
    ) -> None:
        self.object_type = object_type
        self.version_range = version_range
        self.extensions = extensions

    def __repr__(self) -> str:
        return "<{} {} ext={}>".format(
            self.object_type, self.version_range, ",".join(self.extensions)
        )


def _identify_stac_extensions(
    object_type: str, d: Dict[str, Any], version_range: STACVersionRange
) -> List[str]:
    """Identifies extensions for STAC Objects that don't list their
    extensions in a 'stac_extensions' property.

    Returns a list of stac_extensions. May mutate the version_range to update
    min or max version.
    """
    stac_extensions: Set[str] = set([])

    # assets (collection assets)

    if object_type == pystac.STACObjectType.ITEMCOLLECTION:
        if "assets" in d:
            stac_extensions.add("assets")
            version_range.set_min(STACVersionID("0.8.0"))

    # checksum
    if "links" in d:
        found_checksum = False
        for link in d["links"]:
            # Account for old links as dicts
            if isinstance(link, str):
                link_props = cast(Dict[str, Any], d["links"][link]).keys()
            else:
                link_props = cast(Dict[str, Any], link).keys()

            if any(prop.startswith("checksum:") for prop in link_props):
                found_checksum = True
                stac_extensions.add(OldExtensionShortIDs.CHECKSUM.value)
        if not found_checksum:
            if "assets" in d:
                for asset in d["assets"].values():
                    asset_props = cast(Dict[str, Any], asset).keys()
                    if any(prop.startswith("checksum:") for prop in asset_props):
                        found_checksum = True
                        stac_extensions.add(OldExtensionShortIDs.CHECKSUM.value)
        if found_checksum:
            version_range.set_min(STACVersionID("0.6.2"))

    # datacube
    if object_type == pystac.STACObjectType.ITEM:
        if any(k.startswith("cube:") for k in cast(Dict[str, Any], d["properties"])):
            stac_extensions.add(OldExtensionShortIDs.DATACUBE.value)
            version_range.set_min(STACVersionID("0.6.1"))

    # datetime-range (old extension)
    if object_type == pystac.STACObjectType.ITEM:
        if "dtr:start_datetime" in d["properties"]:
            stac_extensions.add("datetime-range")
            version_range.set_min(STACVersionID("0.6.0"))

    # eo
    if object_type == pystac.STACObjectType.ITEM:
        if any(k.startswith("eo:") for k in cast(Dict[str, Any], d["properties"])):
            stac_extensions.add(OldExtensionShortIDs.EO.value)
            if "eo:epsg" in d["properties"]:
                if d["properties"]["eo:epsg"] is None:
                    version_range.set_min(STACVersionID("0.6.1"))
            if "eo:crs" in d["properties"]:
                version_range.set_max(STACVersionID("0.4.1"))
            if "eo:constellation" in d["properties"]:
                version_range.set_min(STACVersionID("0.6.0"))
        if "eo:bands" in d:
            stac_extensions.add(OldExtensionShortIDs.EO.value)
            version_range.set_max(STACVersionID("0.5.2"))

    # pointcloud
    if object_type == pystac.STACObjectType.ITEM:
        if any(k.startswith("pc:") for k in cast(Dict[str, Any], d["properties"])):
            stac_extensions.add(OldExtensionShortIDs.POINTCLOUD.value)
            version_range.set_min(STACVersionID("0.6.2"))

    # sar
    if object_type == pystac.STACObjectType.ITEM:
        if any(k.startswith("sar:") for k in cast(Dict[str, Any], d["properties"])):
            stac_extensions.add(OldExtensionShortIDs.SAR.value)
            version_range.set_min(STACVersionID("0.6.2"))
            if version_range.contains("0.6.2"):
                for prop in [
                    "sar:absolute_orbit",
                    "sar:resolution",
                    "sar:pixel_spacing",
                    "sar:looks",
                ]:
                    if prop in d["properties"]:
                        if isinstance(d["properties"][prop], list):
                            version_range.set_max(STACVersionID("0.6.2"))
            if version_range.contains("0.7.0"):
                for prop in [
                    "sar:incidence_angle",
                    "sar:relative_orbit",
                    "sar:observation_direction",
                    "sar:resolution_range",
                    "sar:resolution_azimuth",
                    "sar:pixel_spacing_range",
                    "sar:pixel_spacing_azimuth",
                    "sar:looks_range",
                    "sar:looks_azimuth",
                    "sar:looks_equivalent_number",
                ]:
                    if prop in d["properties"]:
                        version_range.set_min(STACVersionID("0.7.0"))
                if "sar:absolute_orbit" in d["properties"] and not isinstance(
                    d["properties"]["sar:absolute_orbit"], list
                ):
                    version_range.set_min(STACVersionID("0.7.0"))
            if "sar:off_nadir" in d["properties"]:
                version_range.set_max(STACVersionID("0.6.2"))

    # scientific
    if (
        object_type == pystac.STACObjectType.ITEM
        or object_type == pystac.STACObjectType.COLLECTION
    ):
        if "properties" in d:
            prop_keys = cast(Dict[str, Any], d["properties"]).keys()
            if any(k.startswith("sci:") for k in prop_keys):
                stac_extensions.add(OldExtensionShortIDs.SCIENTIFIC.value)
                version_range.set_min(STACVersionID("0.6.0"))

    # Single File STAC
    if object_type == pystac.STACObjectType.ITEMCOLLECTION:
        if "collections" in d:
            stac_extensions.add(OldExtensionShortIDs.SINGLE_FILE_STAC.value)
            version_range.set_min(STACVersionID("0.8.0"))
            if "stac_extensions" not in d:
                version_range.set_max(STACVersionID("0.8.1"))

    return list(stac_extensions)


def identify_stac_object_type(json_dict: Dict[str, Any]) -> "STACObjectType_Type":
    """Determines the STACObjectType of the provided JSON dict.

    Args:
        json_dict : The dict of STAC JSON to identify.

    Returns:
        STACObjectType: The object type represented by the JSON.
    """
    object_type = None

    if "type" in json_dict:  # Try to identify using 'type' property
        for t in pystac.STACObjectType:
            if json_dict["type"].lower() == t.value.lower():
                object_type = t
                break

    if object_type is None:  # Use old-approach based on other properties
        # Identify pre-1.0 ITEMCOLLECTION (since removed)
        if "type" in json_dict and "assets" not in json_dict:
            if "stac_version" in json_dict and json_dict["stac_version"].startswith(
                "0"
            ):
                if json_dict["type"] == "FeatureCollection":
                    object_type = pystac.STACObjectType.ITEMCOLLECTION

        if "extent" in json_dict:
            object_type = pystac.STACObjectType.COLLECTION
        elif "assets" in json_dict:
            object_type = pystac.STACObjectType.ITEM
        else:
            object_type = pystac.STACObjectType.CATALOG

    return object_type


def identify_stac_object(json_dict: Dict[str, Any]) -> STACJSONDescription:
    """Determines the STACJSONDescription of the provided JSON dict.

    Args:
        json_dict : The dict of STAC JSON to identify.

    Returns:
        STACJSONDescription: The description of the STAC object serialized in the
        given dict.
    """
    object_type = identify_stac_object_type(json_dict)

    version_range = STACVersionRange()

    stac_version = json_dict.get("stac_version")
    stac_extensions = json_dict.get("stac_extensions", None)

    if stac_version is None:
        if (
            object_type == pystac.STACObjectType.CATALOG
            or object_type == pystac.STACObjectType.COLLECTION
        ):
            version_range.set_max(STACVersionID("0.5.2"))
        elif object_type == pystac.STACObjectType.ITEM:
            version_range.set_max(STACVersionID("0.7.0"))
        else:  # ItemCollection
            version_range.set_min(STACVersionID("0.8.0"))
    else:
        version_range.set_to_single(stac_version)

    if stac_extensions is not None:
        version_range.set_min(STACVersionID("0.8.0"))

    if stac_extensions is None:
        # If this is post-0.8, we can assume there are no extensions
        # if the stac_extensions property doesn't exist for everything
        # but ItemCollection (except after 0.9.0, when ItemCollection also got
        # the stac_extensions property).
        if version_range.is_earlier_than("0.8.0") or (
            object_type == pystac.STACObjectType.ITEMCOLLECTION
            and not version_range.is_later_than("0.8.1")
        ):
            stac_extensions = _identify_stac_extensions(
                object_type, json_dict, version_range
            )
        else:
            stac_extensions = []

        # Between 1.0.0-beta.2 and 1.0.0-RC1, STAC extensions changed from
        # being split between 'common' and custom extensions, with common
        # extensions having short names that were used in the stac_extensions
        # property list, to being mostly externalized from the core spec and
        # always identified with the schema URI as the identifier. This
        # code translates the short name IDs used pre-1.0.0-RC1 to the
        # relevant extension schema uri identifier.

    if not version_range.is_single_version():
        # Final Checks

        if "links" in json_dict:
            # links were a dictionary only in 0.5
            if "links" in json_dict and isinstance(json_dict["links"], dict):
                version_range.set_to_single(STACVersionID("0.5.2"))

            # self links became non-required in 0.7.0
            if not version_range.is_earlier_than("0.7.0") and not any(
                filter(
                    lambda l: cast(Dict[str, Any], l)["rel"] == pystac.RelType.SELF,
                    json_dict["links"],
                )
            ):
                version_range.set_min(STACVersionID("0.7.0"))

    return STACJSONDescription(object_type, version_range, set(stac_extensions))
Enumerates the IDs of common extensions. # TODO # TODO Defines STAC versions in an object that is orderable based on version number.     For instance, ``1.0.0-beta.2 < 1.0.0`` # Account for RC or beta releases in version Defines a range of STAC versions. # type:ignore # type:ignore Describes the STAC object information for a STAC object represented in JSON      Attributes:         object_type : Describes the STAC object type. One of             :class:`~pystac.STACObjectType`.         version_range : The STAC version range that describes what             has been identified as potential valid versions of the stac object.         extensions : List of extension schema URIs for extensions this             object implements Identifies extensions for STAC Objects that don't list their     extensions in a 'stac_extensions' property.      Returns a list of stac_extensions. May mutate the version_range to update     min or max version. # assets (collection assets) # checksum # Account for old links as dicts # datacube # datetime-range (old extension) # eo # pointcloud # sar # scientific # Single File STAC Determines the STACObjectType of the provided JSON dict.      Args:         json_dict : The dict of STAC JSON to identify.      Returns:         STACObjectType: The object type represented by the JSON. # Try to identify using 'type' property # Use old-approach based on other properties # Identify pre-1.0 ITEMCOLLECTION (since removed) Determines the STACJSONDescription of the provided JSON dict.      Args:         json_dict : The dict of STAC JSON to identify.      Returns:         STACJSONDescription: The description of the STAC object serialized in the         given dict. # ItemCollection # If this is post-0.8, we can assume there are no extensions # if the stac_extensions property doesn't exist for everything # but ItemCollection (except after 0.9.0, when ItemCollection also got # the stac_extensions property). # Between 1.0.0-beta.2 and 1.0.0-RC1, STAC extensions changed from # being split between 'common' and custom extensions, with common # extensions having short names that were used in the stac_extensions # property list, to being mostly externalized from the core spec and # always identified with the schema URI as the identifier. This # code translates the short name IDs used pre-1.0.0-RC1 to the # relevant extension schema uri identifier. # Final Checks # links were a dictionary only in 0.5 # self links became non-required in 0.7.0