[Git][debian-gis-team/stac-pydantic][master] 5 commits: New upstream version 3.6.0
Antonio Valentino (@antonio.valentino)
gitlab at salsa.debian.org
Sun Jun 28 08:55:50 BST 2026
Antonio Valentino pushed to branch master at Debian GIS Project / stac-pydantic
Commits:
30545b06 by Antonio Valentino at 2026-06-28T07:46:31+00:00
New upstream version 3.6.0
- - - - -
a9366f98 by Antonio Valentino at 2026-06-28T07:46:32+00:00
Update upstream source from tag 'upstream/3.6.0'
Update to upstream version '3.6.0'
with Debian dir 5866510f908404f53a3c9d4ec222b51b4017e140
- - - - -
981bf5d2 by Antonio Valentino at 2026-06-28T07:51:48+00:00
New upstream release
- - - - -
fad31678 by Antonio Valentino at 2026-06-28T07:52:00+00:00
Refresh all patches
- - - - -
9b8ac3ca by Antonio Valentino at 2026-06-28T07:52:58+00:00
Set distribution to unstable
- - - - -
29 changed files:
- .github/workflows/cicd.yml
- .github/workflows/release.yml
- .pre-commit-config.yaml
- CHANGELOG.md
- debian/changelog
- debian/patches/0001-No-coverage.patch
- pyproject.toml
- stac_pydantic/api/collections.py
- stac_pydantic/api/conformance.py
- stac_pydantic/api/extensions/fields.py
- stac_pydantic/api/extensions/query.py
- stac_pydantic/api/item_collection.py
- stac_pydantic/api/landing.py
- stac_pydantic/api/links.py
- stac_pydantic/api/search.py
- stac_pydantic/api/utils/link_factory.py
- stac_pydantic/catalog.py
- stac_pydantic/collection.py
- stac_pydantic/extensions.py
- stac_pydantic/item.py
- stac_pydantic/item_collection.py
- stac_pydantic/links.py
- stac_pydantic/shared.py
- stac_pydantic/utils.py
- stac_pydantic/version.py
- tests/api/test_link_factory.py
- tests/conftest.py
- tests/test_models.py
- uv.lock
Changes:
=====================================
.github/workflows/cicd.yml
=====================================
@@ -17,8 +17,6 @@ jobs:
strategy:
matrix:
python-version:
- - '3.8'
- - '3.9'
- '3.10'
- '3.11'
- '3.12'
@@ -29,7 +27,7 @@ jobs:
- uses: actions/checkout at de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install uv
- uses: astral-sh/setup-uv at 08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
+ uses: astral-sh/setup-uv at fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
version: "0.9.*"
enable-cache: true
@@ -48,7 +46,7 @@ jobs:
- name: Upload Results
if: ${{ matrix.python-version == env.LATEST_PY_VERSION }}
- uses: codecov/codecov-action at e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
+ uses: codecov/codecov-action at fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
files: ./coverage.xml
flags: unittests
=====================================
.github/workflows/release.yml
=====================================
@@ -15,7 +15,7 @@ jobs:
- uses: actions/checkout at de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install uv
- uses: astral-sh/setup-uv at 08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
+ uses: astral-sh/setup-uv at fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
version: "0.9.*"
enable-cache: true
=====================================
.pre-commit-config.yaml
=====================================
@@ -20,14 +20,14 @@ repos:
language_version: python
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.15.12
+ rev: v0.15.19
hooks:
- id: ruff
args: ["--fix"]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
- rev: v1.20.2
+ rev: v2.1.0
hooks:
- id: mypy
language_version: python
=====================================
CHANGELOG.md
=====================================
@@ -1,6 +1,20 @@
## Unreleased
+## 3.6.0 (2026-06-26)
+
+- No change since 3.6.0a1
+
+## 3.6.0a1 (2026-06-25)
+
+- fix: invalid urljoin usage in Link model `resolve` method
+
+## 3.6.0a0 (2026-06-24)
+
+- update: type hints for python >=3.10
+- fix: invalid urljoin usage in link factories (#219, @vincentsarago)
+- remove: python 3.8 and 3.9 support
+
## 3.5.2 (2026-05-29)
- fix: remove wrong longitude check for antimeridian crossing bbox validation (#212, @vincentsarago)
=====================================
debian/changelog
=====================================
@@ -1,3 +1,11 @@
+stac-pydantic (3.6.0-1) unstable; urgency=medium
+
+ * New upstream release.
+ * debian/patches:
+ - Refresh all patches.
+
+ -- Antonio Valentino <antonio.valentino at tiscali.it> Sun, 28 Jun 2026 07:52:04 +0000
+
stac-pydantic (3.5.2-1) unstable; urgency=medium
[ Bas Couwenberg ]
=====================================
debian/patches/0001-No-coverage.patch
=====================================
@@ -10,10 +10,10 @@ Forwarded: not-needed
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
-index 48f0032..313e161 100644
+index b9308bd..cf3cd1e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
-@@ -76,7 +76,7 @@ only-include = ["stac_pydantic/"]
+@@ -75,7 +75,7 @@ only-include = ["stac_pydantic/"]
only-include = ["stac_pydantic/"]
[tool.pytest.ini_options]
=====================================
pyproject.toml
=====================================
@@ -6,8 +6,6 @@ classifiers=[
"Intended Audience :: Developers",
"Intended Audience :: Information Technology",
"Intended Audience :: Science/Research",
- "Programming Language :: Python :: 3.8",
- "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
@@ -24,12 +22,13 @@ maintainers = [
{ name = "Pete Gadomski", email = "pete.gadomski at gmail.com" },
]
license = {file = "LICENSE"}
-requires-python=">=3.8"
+requires-python=">=3.10"
dynamic = ["version"]
dependencies = [
"click>=8.1.7",
"pydantic>=2.4.1",
"geojson-pydantic>=1.0.0",
+ "typing-extensions>=4.0.0; python_version < '3.11'",
]
[project.scripts]
@@ -99,6 +98,7 @@ select = [
"F",
"W",
"B",
+ "UP",
]
ignore = [
"E203", # line too long, handled by black
=====================================
stac_pydantic/api/collections.py
=====================================
@@ -1,5 +1,3 @@
-from typing import List, Optional
-
from pydantic import model_validator
from stac_pydantic.api.collection import Collection
@@ -15,9 +13,9 @@ class Collections(StacBaseModel):
"""
links: Links
- collections: List[Collection]
- numberMatched: Optional[int] = None
- numberReturned: Optional[int] = None
+ collections: list[Collection]
+ numberMatched: int | None = None
+ numberReturned: int | None = None
@model_validator(mode="after")
def required_links(self) -> "Collections":
=====================================
stac_pydantic/api/conformance.py
=====================================
@@ -1,5 +1,3 @@
-from typing import List
-
from pydantic import AnyHttpUrl, BaseModel
@@ -8,4 +6,4 @@ class Conformance(BaseModel):
https://github.com/radiantearth/stac-api-spec/blob/master/api-spec.md#ogc-api---features-endpoints
"""
- conformsTo: List[AnyHttpUrl]
+ conformsTo: list[AnyHttpUrl]
=====================================
stac_pydantic/api/extensions/fields.py
=====================================
@@ -1,5 +1,3 @@
-from typing import Dict, Optional, Set
-
from pydantic import BaseModel
@@ -8,10 +6,10 @@ class FieldsExtension(BaseModel):
https://github.com/radiantearth/stac-api-spec/tree/master/extensions/fields#fields-api-extension
"""
- includes: Optional[Set[str]] = None
- excludes: Optional[Set[str]] = None
+ includes: set[str] | None = None
+ excludes: set[str] | None = None
- def _get_field_dict(self, fields: Set[str]) -> Dict[str, Set[str]]:
+ def _get_field_dict(self, fields: set[str]) -> dict[str, set[str]]:
"""Internal method to create a dictionary for advanced include or exclude of pydantic fields on model export
Ref: https://pydantic-docs.helpmanual.io/usage/exporting_models/#advanced-include-and-exclude
@@ -29,7 +27,7 @@ class FieldsExtension(BaseModel):
return field_dict
@property
- def filter(self) -> Dict[str, Dict[str, Set[str]]]:
+ def filter(self) -> dict[str, dict[str, set[str]]]:
"""
Create dictionary of fields to include/exclude on model export based on the included and excluded fields passed
to the API. The output of this property may be passed to pydantic's serialization methods to include or exclude
@@ -37,7 +35,7 @@ class FieldsExtension(BaseModel):
Ref: https://pydantic-docs.helpmanual.io/usage/exporting_models/#advanced-include-and-exclude
"""
- include: Set[str] = set()
+ include: set[str] = set()
# If only include is specified, add fields to the set
if self.includes and not self.excludes:
include = include.union(self.includes)
@@ -46,5 +44,5 @@ class FieldsExtension(BaseModel):
include = include.union(self.includes) - self.excludes
return {
"include": self._get_field_dict(include),
- "exclude": self._get_field_dict(self.excludes or Set()),
+ "exclude": self._get_field_dict(self.excludes or set()),
}
=====================================
stac_pydantic/api/extensions/query.py
=====================================
@@ -1,9 +1,10 @@
"""Query Extension."""
import warnings
+from collections.abc import Callable
from enum import auto
from types import DynamicClassAttribute
-from typing import Any, Callable
+from typing import Any
from stac_pydantic.utils import AutoValueEnum
=====================================
stac_pydantic/api/item_collection.py
=====================================
@@ -1,4 +1,4 @@
-from typing import Optional, Sequence
+from collections.abc import Sequence
from urllib.parse import urlparse
from pydantic import model_validator
@@ -16,9 +16,9 @@ class ItemCollection(BaseItemCollection):
"""
features: Sequence[Item]
- links: Optional[Links] = None
- numberMatched: Optional[int] = None
- numberReturned: Optional[int] = None
+ links: Links | None = None
+ numberMatched: int | None = None
+ numberReturned: int | None = None
@model_validator(mode="after")
def required_links(self) -> "ItemCollection":
=====================================
stac_pydantic/api/landing.py
=====================================
@@ -1,5 +1,3 @@
-from typing import List
-
from pydantic import AnyUrl, HttpUrl, model_validator
from stac_pydantic.api.links import Links
@@ -15,7 +13,7 @@ class LandingPage(Catalog):
https://github.com/radiantearth/stac-api-spec/tree/v1.0.0/item-search#link-relations
"""
- conformsTo: List[AnyUrl] = [
+ conformsTo: list[AnyUrl] = [
HttpUrl("https://api.stacspec.org/v1.0.0/core"),
HttpUrl("http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core"),
]
=====================================
stac_pydantic/api/links.py
=====================================
@@ -1,4 +1,4 @@
-from typing import Any, Dict, List, Literal, Optional, Union
+from typing import Any, Literal
from pydantic import Field
@@ -14,8 +14,8 @@ class _MethodLink(Link):
"""
method: Literal["GET", "POST"] = "GET"
- headers: Optional[Dict[str, Any]] = None
- body: Optional[Dict[Any, Any]] = None
+ headers: dict[str, Any] | None = None
+ body: dict[Any, Any] | None = None
merge: bool = False
@@ -44,4 +44,4 @@ class ItemsLink(_MethodLink):
class Links(BaseLinks):
- root: List[Union[SearchLink, PaginationLink, ItemsLink, Link]]
+ root: list[SearchLink | PaginationLink | ItemsLink | Link]
=====================================
stac_pydantic/api/search.py
=====================================
@@ -1,5 +1,5 @@
from datetime import datetime as dt
-from typing import Any, Dict, List, Optional, Union
+from typing import Annotated, Any, TypeAlias
from geojson_pydantic.geometries import (
GeometryCollection,
@@ -11,7 +11,6 @@ from geojson_pydantic.geometries import (
Polygon,
)
from pydantic import AfterValidator, BaseModel, Field, model_validator
-from typing_extensions import Annotated
from stac_pydantic.api.extensions.fields import FieldsExtension
from stac_pydantic.api.extensions.query import Operator
@@ -26,15 +25,15 @@ from stac_pydantic.shared import (
validate_datetime,
)
-Intersection = Union[
- Point,
- MultiPoint,
- LineString,
- MultiLineString,
- Polygon,
- MultiPolygon,
- GeometryCollection,
-]
+Intersection: TypeAlias = (
+ Point
+ | MultiPoint
+ | LineString
+ | MultiLineString
+ | Polygon
+ | MultiPolygon
+ | GeometryCollection
+)
class Search(BaseModel):
@@ -44,23 +43,23 @@ class Search(BaseModel):
https://github.com/radiantearth/stac-api-spec/blob/v1.0.0/item-search/README.md#query-parameter-table
"""
- collections: Optional[List[str]] = None
- ids: Optional[List[str]] = None
- bbox: Annotated[Optional[BBox], AfterValidator(validate_bbox)] = None
- intersects: Optional[Intersection] = None
- datetime: Annotated[Optional[str], AfterValidator(validate_datetime)] = None
- limit: Optional[int] = 10
+ collections: list[str] | None = None
+ ids: list[str] | None = None
+ bbox: Annotated[BBox | None, AfterValidator(validate_bbox)] = None
+ intersects: Intersection | None = None
+ datetime: Annotated[str | None, AfterValidator(validate_datetime)] = None
+ limit: int | None = 10
@property
- def start_date(self) -> Optional[dt]:
- start_date: Optional[dt] = None
+ def start_date(self) -> dt | None:
+ start_date: dt | None = None
if self.datetime:
start_date = str_to_datetimes(self.datetime)[0]
return start_date
@property
- def end_date(self) -> Optional[dt]:
- end_date: Optional[dt] = None
+ def end_date(self) -> dt | None:
+ end_date: dt | None = None
if self.datetime:
dates = str_to_datetimes(self.datetime)
end_date = dates[0] if len(dates) == 1 else dates[1]
@@ -68,13 +67,13 @@ class Search(BaseModel):
# Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information.
@model_validator(mode="before")
- def validate_spatial(cls, values: Dict[str, Any]) -> Dict[str, Any]:
+ def validate_spatial(cls, values: dict[str, Any]) -> dict[str, Any]:
if values.get("intersects") and values.get("bbox") is not None:
raise ValueError("intersects and bbox parameters are mutually exclusive")
return values
@property
- def spatial_filter(self) -> Optional[Intersection]:
+ def spatial_filter(self) -> Intersection | None:
"""Return a geojson-pydantic object representing the spatial filter for the search request.
Check for both because the ``bbox`` and ``intersects`` parameters are mutually exclusive.
@@ -92,6 +91,6 @@ class ExtendedSearch(Search):
STAC API search with extensions enabled.
"""
- field: Optional[FieldsExtension] = Field(None, alias="fields")
- query: Optional[Dict[str, Dict[Operator, Any]]] = None
- sortby: Optional[List[SortExtension]] = None
+ field: FieldsExtension | None = Field(None, alias="fields")
+ query: dict[str, dict[Operator, Any]] | None = None
+ sortby: list[SortExtension] | None = None
=====================================
stac_pydantic/api/utils/link_factory.py
=====================================
@@ -1,6 +1,6 @@
from dataclasses import dataclass
-from typing import ClassVar, Tuple
-from urllib.parse import urljoin
+from typing import ClassVar
+from urllib.parse import urljoin, urlsplit
from stac_pydantic.links import Link, Links, Relations
from stac_pydantic.shared import MimeTypes
@@ -11,12 +11,15 @@ class BaseLinks:
"""Create inferred links common to collections and items."""
base_url: str
- _link_members: ClassVar[Tuple[str, ...]] = ("root",)
+ _link_members: ClassVar[tuple[str, ...]] = ("root",)
def root(self) -> Link:
"""Return the catalog root."""
+ path = urlsplit(self.base_url).path.rstrip("/")
return Link(
- rel=Relations.root, type=MimeTypes.json, href=urljoin(self.base_url, "/")
+ rel=Relations.root,
+ type=MimeTypes.json,
+ href=urljoin(self.base_url, f"{path}/"),
)
def create_links(self) -> Links:
@@ -31,28 +34,35 @@ class CollectionLinks(BaseLinks):
"""Create inferred links specific to collections."""
collection_id: str
- _link_members: ClassVar[Tuple[str, ...]] = ("root", "self", "parent", "items")
+ _link_members: ClassVar[tuple[str, ...]] = ("root", "self", "parent", "items")
def self(self) -> Link:
"""Create the `self` link."""
+ path = urlsplit(self.base_url).path.rstrip("/")
return Link(
rel=Relations.self,
type=MimeTypes.json,
- href=urljoin(self.base_url, f"/collections/{self.collection_id}"),
+ href=urljoin(self.base_url, f"{path}/collections/{self.collection_id}"),
)
def parent(self) -> Link:
"""Create the `parent` link."""
+ path = urlsplit(self.base_url).path.rstrip("/")
return Link(
- rel=Relations.parent, type=MimeTypes.json, href=urljoin(self.base_url, "/")
+ rel=Relations.parent,
+ type=MimeTypes.json,
+ href=urljoin(self.base_url, f"{path}/"),
)
def items(self) -> Link:
"""Create the `item` link."""
+ path = urlsplit(self.base_url).path.rstrip("/")
return Link(
rel=Relations.items,
type=MimeTypes.geojson,
- href=urljoin(self.base_url, f"/collections/{self.collection_id}/items"),
+ href=urljoin(
+ self.base_url, f"{path}/collections/{self.collection_id}/items"
+ ),
)
@@ -62,30 +72,34 @@ class ItemLinks(BaseLinks):
collection_id: str
item_id: str
- _link_members: ClassVar[Tuple[str, ...]] = ("root", "self", "parent", "collection")
+ _link_members: ClassVar[tuple[str, ...]] = ("root", "self", "parent", "collection")
def self(self) -> Link:
"""Create the `self` link."""
+ path = urlsplit(self.base_url).path.rstrip("/")
return Link(
rel=Relations.self,
type=MimeTypes.geojson,
href=urljoin(
- self.base_url, f"/collections/{self.collection_id}/items/{self.item_id}"
+ self.base_url,
+ f"{path}/collections/{self.collection_id}/items/{self.item_id}",
),
)
def parent(self) -> Link:
"""Create the `parent` link."""
+ path = urlsplit(self.base_url).path.rstrip("/")
return Link(
rel=Relations.parent,
type=MimeTypes.json,
- href=urljoin(self.base_url, f"/collections/{self.collection_id}"),
+ href=urljoin(self.base_url, f"{path}/collections/{self.collection_id}"),
)
def collection(self) -> Link:
"""Create the `collection` link."""
+ path = urlsplit(self.base_url).path.rstrip("/")
return Link(
rel=Relations.collection,
type=MimeTypes.json,
- href=urljoin(self.base_url, f"/collections/{self.collection_id}"),
+ href=urljoin(self.base_url, f"{path}/collections/{self.collection_id}"),
)
=====================================
stac_pydantic/catalog.py
=====================================
@@ -1,4 +1,4 @@
-from typing import List, Literal, Optional
+from typing import Literal
from pydantic import AnyUrl, ConfigDict, Field
@@ -16,8 +16,8 @@ class _Catalog(StacBaseModel):
description: str = Field(..., alias="description", min_length=1)
stac_version: str = Field(STAC_VERSION, pattern=SEMVER_REGEX)
links: Links
- stac_extensions: Optional[List[AnyUrl]] = []
- title: Optional[str] = None
+ stac_extensions: list[AnyUrl] | None = []
+ title: str | None = None
type: str
model_config = ConfigDict(use_enum_values=True, extra="allow")
=====================================
stac_pydantic/collection.py
=====================================
@@ -1,7 +1,6 @@
-from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union
+from typing import TYPE_CHECKING, Annotated, Any, Literal
from pydantic import AfterValidator, Field, conlist
-from typing_extensions import Annotated
from stac_pydantic.catalog import _Catalog
from stac_pydantic.shared import (
@@ -15,14 +14,14 @@ from stac_pydantic.shared import (
)
if TYPE_CHECKING:
- StartEndTime = List[Union[UtcDatetime, None]]
- TInterval = List[StartEndTime]
+ StartEndTime = list[UtcDatetime | None]
+ TInterval = list[StartEndTime]
else:
- StartEndTime = conlist(Union[UtcDatetime, None], min_length=2, max_length=2)
+ StartEndTime = conlist(UtcDatetime | None, min_length=2, max_length=2)
TInterval = conlist(StartEndTime, min_length=1)
-def validate_bbox_interval(v: List[BBox]) -> List[BBox]: # noqa: C901
+def validate_bbox_interval(v: list[BBox]) -> list[BBox]: # noqa: C901
ivalues = iter(v)
# The first time interval always describes the overall spatial extent of the data.
@@ -150,7 +149,7 @@ class SpatialExtent(StacBaseModel):
https://github.com/radiantearth/stac-spec/blob/v1.0.0/collection-spec/collection-spec.md#spatial-extent-object
"""
- bbox: Annotated[List[BBox], AfterValidator(validate_bbox_interval)]
+ bbox: Annotated[list[BBox], AfterValidator(validate_bbox_interval)]
class TimeInterval(StacBaseModel):
@@ -175,8 +174,8 @@ class Range(StacBaseModel):
https://github.com/radiantearth/stac-spec/blob/v1.0.0/collection-spec/collection-spec.md#stats-object
"""
- minimum: Union[NumType, str]
- maximum: Union[NumType, str]
+ minimum: NumType | str
+ maximum: NumType | str
class Collection(_Catalog):
@@ -184,11 +183,11 @@ class Collection(_Catalog):
https://github.com/radiantearth/stac-spec/blob/v1.0.0/collection-spec/collection-spec.md
"""
- assets: Optional[Dict[str, Asset]] = None
+ assets: dict[str, Asset] | None = None
license: str = Field(..., alias="license", min_length=1)
extent: Extent
- title: Optional[str] = None
- keywords: Optional[List[str]] = None
- providers: Optional[List[Provider]] = None
- summaries: Optional[Dict[str, Union[Range, List[Any], Dict[str, Any]]]] = None
+ title: str | None = None
+ keywords: list[str] | None = None
+ providers: list[Provider] | None = None
+ summaries: dict[str, Range | list[Any] | dict[str, Any]] | None = None
type: Literal["Collection"]
=====================================
stac_pydantic/extensions.py
=====================================
@@ -1,5 +1,5 @@
from functools import lru_cache
-from typing import Any, Dict, Union
+from typing import Any
from stac_pydantic.catalog import Catalog
from stac_pydantic.collection import Collection
@@ -24,7 +24,7 @@ def _fetch_and_cache_schema(url: str) -> dict:
def validate_extensions(
- stac_obj: Union[Item, Collection, Catalog, Dict[str, Any]],
+ stac_obj: Item | Collection | Catalog | dict[str, Any],
reraise_exception: bool = False,
) -> bool:
"""
=====================================
stac_pydantic/item.py
=====================================
@@ -1,4 +1,4 @@
-from typing import Any, Dict, List, Optional
+from typing import Any
from geojson_pydantic import Feature
from pydantic import (
@@ -32,14 +32,14 @@ class Item(Feature, StacBaseModel):
id: str = Field(..., alias="id", min_length=1)
stac_version: str = Field(STAC_VERSION, pattern=SEMVER_REGEX)
properties: ItemProperties
- assets: Dict[str, Asset]
+ assets: dict[str, Asset]
links: Links
- stac_extensions: Optional[List[AnyUrl]] = []
- collection: Optional[str] = None
+ stac_extensions: list[AnyUrl] | None = []
+ collection: str | None = None
@model_validator(mode="before")
@classmethod
- def validate_bbox(cls, values: Dict[str, Any]) -> Dict[str, Any]:
+ def validate_bbox(cls, values: dict[str, Any]) -> dict[str, Any]:
if isinstance(values, dict):
if values.get("geometry") and values.get("bbox") is None:
raise ValueError("bbox is required if geometry is not null")
=====================================
stac_pydantic/item_collection.py
=====================================
@@ -1,4 +1,4 @@
-from typing import Optional, Sequence
+from collections.abc import Sequence
from geojson_pydantic import FeatureCollection
@@ -16,4 +16,4 @@ class ItemCollection(FeatureCollection, StacBaseModel):
"""
features: Sequence[Item] # type: ignore
- links: Optional[Links] = None
+ links: Links | None = None
=====================================
stac_pydantic/links.py
=====================================
@@ -1,6 +1,6 @@
+from collections.abc import Iterator
from enum import auto
-from typing import Iterator, List, Optional, Union
-from urllib.parse import urljoin
+from urllib.parse import urljoin, urlsplit
from pydantic import ConfigDict, Field, RootModel
@@ -15,18 +15,20 @@ class Link(StacBaseModel):
href: str = Field(..., alias="href", min_length=1)
rel: str = Field(..., alias="rel", min_length=1)
- type: Optional[Union[MimeTypes, str]] = None
- title: Optional[str] = None
+ type: MimeTypes | str | None = None
+ title: str | None = None
model_config = ConfigDict(use_enum_values=True, extra="allow")
def resolve(self, base_url: str) -> None:
"""resolve a link to the given base URL"""
- self.href = urljoin(base_url, self.href)
+ path = urlsplit(base_url).path.rstrip("/")
+ href = self.href.lstrip("/")
+ self.href = urljoin(base_url, f"{path}/{href}")
-class Links(RootModel[List[Link]]):
- root: List[Link]
+class Links(RootModel[list[Link]]):
+ root: list[Link]
def link_iterator(self) -> Iterator[Link]:
"""Produce iterator to iterate through links"""
=====================================
stac_pydantic/shared.py
=====================================
@@ -1,7 +1,8 @@
+import sys
from datetime import datetime as dt
from datetime import timezone
from enum import Enum, auto
-from typing import Any, Dict, List, Optional, Tuple, Union, cast
+from typing import Annotated, Any, cast
from warnings import warn
from pydantic import (
@@ -16,15 +17,19 @@ from pydantic import (
model_serializer,
model_validator,
)
-from typing_extensions import Annotated, Self
+
+if sys.version_info < (3, 11):
+ from typing_extensions import Self
+else:
+ from typing import Self
from stac_pydantic.utils import AutoValueEnum
-NumType = Union[float, int]
-BBox = Union[
- Tuple[NumType, NumType, NumType, NumType], # 2D bbox
- Tuple[NumType, NumType, NumType, NumType, NumType, NumType], # 3D bbox
-]
+NumType = float | int
+BBox = (
+ tuple[NumType, NumType, NumType, NumType]
+ | tuple[NumType, NumType, NumType, NumType, NumType, NumType]
+)
SEMVER_REGEX = r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"
@@ -37,7 +42,7 @@ UtcDatetime = Annotated[
AfterValidator(lambda d: d.astimezone(timezone.utc)),
]
-SearchDatetime: TypeAdapter = TypeAdapter(Optional[UtcDatetime])
+SearchDatetime: TypeAdapter = TypeAdapter(UtcDatetime | None)
class MimeTypes(str, Enum):
@@ -97,7 +102,7 @@ class ProviderRoles(str, AutoValueEnum):
class StacBaseModel(BaseModel):
def to_dict(
self, by_alias: bool = True, exclude_unset: bool = True, **kwargs: Any
- ) -> Dict[str, Any]:
+ ) -> dict[str, Any]:
warn(
"`to_dict` method is deprecated. Use `model_dump` instead",
DeprecationWarning,
@@ -117,7 +122,7 @@ class StacBaseModel(BaseModel):
def model_dump( # type: ignore[override]
self, *, by_alias: bool = True, exclude_unset: bool = True, **kwargs: Any
- ) -> Dict[str, Any]:
+ ) -> dict[str, Any]:
return super().model_dump(
by_alias=by_alias, exclude_unset=exclude_unset, **kwargs
)
@@ -136,9 +141,9 @@ class Provider(StacBaseModel):
"""
name: str = Field(..., min_length=1)
- description: Optional[str] = None
- roles: Optional[List[str]] = None
- url: Optional[str] = None
+ description: str | None = None
+ roles: list[str] | None = None
+ url: str | None = None
class StacCommonMetadata(StacBaseModel):
@@ -147,25 +152,25 @@ class StacCommonMetadata(StacBaseModel):
"""
# Basic
- title: Optional[str] = None
- description: Optional[str] = None
+ title: str | None = None
+ description: str | None = None
# Date and Time
- datetime: Optional[UtcDatetime] = Field(...)
- created: Optional[UtcDatetime] = None
- updated: Optional[UtcDatetime] = None
+ datetime: UtcDatetime | None = Field(...)
+ created: UtcDatetime | None = None
+ updated: UtcDatetime | None = None
# Date and Time Range
- start_datetime: Optional[UtcDatetime] = None
- end_datetime: Optional[UtcDatetime] = None
+ start_datetime: UtcDatetime | None = None
+ end_datetime: UtcDatetime | None = None
# Licensing
- license: Optional[str] = None
+ license: str | None = None
# Provider
- providers: Optional[List[Provider]] = None
+ providers: list[Provider] | None = None
# Instrument
- platform: Optional[str] = None
- instruments: Optional[List[str]] = None
- constellation: Optional[str] = None
- mission: Optional[str] = None
- gsd: Optional[float] = Field(None, gt=0)
+ platform: str | None = None
+ instruments: list[str] | None = None
+ constellation: str | None = None
+ mission: str | None = None
+ gsd: float | None = Field(None, gt=0)
@model_validator(mode="after")
def validate_datetime_or_start_end(self) -> Self:
@@ -211,23 +216,23 @@ class Asset(StacBaseModel):
"""
href: str = Field(..., min_length=1)
- type: Optional[str] = None
- title: Optional[str] = None
- description: Optional[str] = None
- roles: Optional[List[str]] = None
+ type: str | None = None
+ title: str | None = None
+ description: str | None = None
+ roles: list[str] | None = None
model_config = ConfigDict(
populate_by_name=True, use_enum_values=True, extra="allow"
)
-def str_to_datetimes(value: str) -> List[Optional[dt]]:
+def str_to_datetimes(value: str) -> list[dt | None]:
# Split on "/" and replace no value or ".." with None
values = [v if v and v != ".." else None for v in value.split("/")]
# Cast because pylance gets confused by the type adapter and annotated type
dates = cast(
- List[Optional[dt]],
+ list[dt | None],
[
# Use the type adapter to validate the datetime strings, strict is necessary
# due to pydantic issues #8736 and #8762
@@ -238,7 +243,7 @@ def str_to_datetimes(value: str) -> List[Optional[dt]]:
return dates
-def validate_datetime(v: Optional[str]) -> Optional[str]:
+def validate_datetime(v: str | None) -> str | None:
"""Validate Datetime value."""
if v is not None:
dates = str_to_datetimes(v)
@@ -263,16 +268,16 @@ def validate_datetime(v: Optional[str]) -> Optional[str]:
return v
-def validate_bbox(v: Optional[BBox]) -> Optional[BBox]:
+def validate_bbox(v: BBox | None) -> BBox | None:
"""Validate BBOX value."""
if v:
# Validate order
if len(v) == 4:
- xmin, ymin, xmax, ymax = cast(Tuple[int, int, int, int], v)
+ xmin, ymin, xmax, ymax = cast(tuple[int, int, int, int], v)
elif len(v) == 6:
xmin, ymin, min_elev, xmax, ymax, max_elev = cast(
- Tuple[int, int, int, int, int, int], v
+ tuple[int, int, int, int, int, int], v
)
if max_elev < min_elev:
raise ValueError(
=====================================
stac_pydantic/utils.py
=====================================
@@ -1,9 +1,9 @@
from enum import Enum
-from typing import Any, List
+from typing import Any
class AutoValueEnum(Enum):
def _generate_next_value_( # type: ignore
- name: str, start: int, count: int, last_values: List[Any]
+ name: str, start: int, count: int, last_values: list[Any]
) -> Any:
return name
=====================================
stac_pydantic/version.py
=====================================
@@ -1,5 +1,5 @@
"""stac-pydantic and STAC spec versions."""
-__version__ = "3.5.2"
+__version__ = "3.6.0"
STAC_VERSION = "1.0.0"
=====================================
tests/api/test_link_factory.py
=====================================
@@ -1,42 +1,88 @@
-from typing import ClassVar, Tuple
-from urllib.parse import urljoin
+from typing import ClassVar
+from urllib.parse import urljoin, urlsplit
+
+import pytest
from stac_pydantic.api.utils.link_factory import BaseLinks, CollectionLinks, ItemLinks
from stac_pydantic.links import Link
from stac_pydantic.shared import MimeTypes
-def test_collection_links() -> None:
+ at pytest.mark.parametrize(
+ "base_url",
+ [
+ "http://stac.com",
+ "http://stac.com/v1/stac",
+ ],
+)
+def test_base_links(base_url) -> None:
+ links = BaseLinks(
+ base_url=base_url,
+ ).create_links()
+ for link in links.link_iterator():
+ assert isinstance(link, Link)
+ assert link.rel in BaseLinks._link_members
+ assert link.href.startswith(base_url)
+
+
+ at pytest.mark.parametrize(
+ "base_url",
+ [
+ "http://stac.com",
+ "http://stac.com/v1/stac",
+ ],
+)
+def test_collection_links(base_url) -> None:
links = CollectionLinks(
- collection_id="collection", base_url="http://stac.com"
+ collection_id="collection",
+ base_url=base_url,
).create_links()
for link in links.link_iterator():
assert isinstance(link, Link)
assert link.rel in CollectionLinks._link_members
+ assert link.href.startswith(base_url)
-def test_item_links() -> None:
+ at pytest.mark.parametrize(
+ "base_url",
+ [
+ "http://stac.com",
+ "http://stac.com/v1/stac",
+ ],
+)
+def test_item_links(base_url) -> None:
links = ItemLinks(
- collection_id="collection", item_id="item", base_url="http://stac.com"
+ collection_id="collection",
+ item_id="item",
+ base_url=base_url,
).create_links()
for link in links.link_iterator():
assert isinstance(link, Link)
assert link.rel in ItemLinks._link_members
+ assert link.href.startswith(base_url)
-def test_custom_links() -> None:
+ at pytest.mark.parametrize(
+ "base_url,expected_href",
+ [
+ ("http://stac.com", "http://stac.com/another-link"),
+ ("http://stac.com/v1/stac", "http://stac.com/v1/stac/another-link"),
+ ],
+)
+def test_custom_links(base_url, expected_href) -> None:
class CustomLinks(BaseLinks):
- _link_members: ClassVar[Tuple[str]] = ("another_link",)
+ _link_members: ClassVar[tuple[str]] = ("another_link",)
def another_link(self) -> Link:
+ path = urlsplit(self.base_url).path.rstrip("/")
return Link(
rel="another-link",
type=MimeTypes.json,
- href=urljoin(self.base_url, "/another-link"),
+ href=urljoin(self.base_url, f"{path}/another-link"),
)
- links = CustomLinks(base_url="http://stac.com").create_links()
+ links = CustomLinks(base_url=base_url).create_links()
assert len(links) == 1
assert links[0].rel == "another-link"
assert links[0].type == "application/json"
- assert links[0].href == "http://stac.com/another-link"
+ assert links[0].href == expected_href
=====================================
tests/conftest.py
=====================================
@@ -1,7 +1,6 @@
import json
import os
from copy import deepcopy
-from typing import List, Optional, Type
import dictdiffer
import pytest
@@ -12,7 +11,7 @@ from pydantic import BaseModel, TypeAdapter
from stac_pydantic.shared import UtcDatetime
-def request(url: str, path: Optional[List[str]] = None):
+def request(url: str, path: list[str] | None = None):
if path is None:
path = ["tests", "example_stac"]
@@ -24,7 +23,7 @@ def request(url: str, path: Optional[List[str]] = None):
_full_path = deepcopy(path)
_full_path.append(url)
full_path = os.path.join(*_full_path)
- with open(full_path, "r") as local_file:
+ with open(full_path) as local_file:
lines = local_file.readlines()
full_file = "".join(lines)
return json.loads(full_file)
@@ -65,9 +64,9 @@ def dict_match(d1: dict, d2: dict):
def compare_example(
example_url: str,
- model: Type[BaseModel],
- fields: Optional[List[str]] = None,
- path: Optional[List[str]] = None,
+ model: type[BaseModel],
+ fields: list[str] | None = None,
+ path: list[str] | None = None,
) -> None:
if path is None:
path = ["tests", "example_stac"]
=====================================
tests/test_models.py
=====================================
@@ -365,13 +365,20 @@ def test_resolve_link() -> None:
assert link.href == "http://base_url.com/hello/world"
-def test_resolve_links() -> None:
+ at pytest.mark.parametrize(
+ "base_url,expected_href",
+ [
+ ("http://base_url.com", "http://base_url.com/hello/world"),
+ ("http://base_url.com/v1/stac", "http://base_url.com/v1/stac/hello/world"),
+ ],
+)
+def test_resolve_links(base_url, expected_href) -> None:
links = Links.model_validate(
[Link(href="/hello/world", type=MimeTypes.jpeg, rel="test")]
)
- links.resolve(base_url="http://base_url.com")
+ links.resolve(base_url=base_url)
for link in links.link_iterator():
- assert link.href == "http://base_url.com/hello/world"
+ assert link.href == expected_href
def test_geometry_null_item() -> None:
=====================================
uv.lock
=====================================
The diff for this file was not included because it is too large.
View it on GitLab: https://salsa.debian.org/debian-gis-team/stac-pydantic/-/compare/5a92ade451dfb403a5179719180b7c3f8b935b6e...9b8ac3ca972a0ee30fae4a6110fec3fa551895c7
--
View it on GitLab: https://salsa.debian.org/debian-gis-team/stac-pydantic/-/compare/5a92ade451dfb403a5179719180b7c3f8b935b6e...9b8ac3ca972a0ee30fae4a6110fec3fa551895c7
You're receiving this email because of your account on salsa.debian.org. Manage all notifications: https://salsa.debian.org/-/profile/notifications | Help: https://salsa.debian.org/help
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20260628/9acd21ce/attachment-0001.htm>
More information about the Pkg-grass-devel
mailing list