[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