diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 1de2131..af44edd 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.8] + python-version: ["3.7", "3.8", "3.9", "3.10"] steps: - uses: actions/checkout@v2 @@ -29,11 +29,6 @@ jobs: python -m pip install --upgrade pip python -m pip install tox codecov pre-commit - # Run pre-commit (only for python-3.7) - - name: run pre-commit - if: matrix.python-version == 3.7 - run: pre-commit run --all-files - # Run tox using the version of Python in `PATH` - name: Run Tox run: tox -e py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1fc7cc7..0775efe 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,12 +7,21 @@ repos: args: ["--safe"] - repo: https://github.com/PyCQA/isort - rev: 5.4.2 + rev: 5.10.1 hooks: - id: isort language_version: python3 - - repo: https://github.com/PyCQA/isort - rev: 5.4.2 + + - repo: https://github.com/PyCQA/flake8 + rev: 4.0.1 hooks: - - id: isort + - id: flake8 language_version: python3 + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v0.931 + hooks: + - id: mypy + exclude: ^tests/ + args: [--strict] + additional_dependencies: ["pydantic", "types-jsonschema", "types-setuptools", "types-requests"] \ No newline at end of file diff --git a/README.md b/README.md index 94a49a6..5f3f770 100644 --- a/README.md +++ b/README.md @@ -11,32 +11,57 @@ For local development: pip install -e .["dev"] ``` -| stac-pydantic | stac | -|-------------------|--------------| -| 1.1.x | 0.9.0 | -| 1.2.x | 1.0.0-beta.1 | -| 1.3.x | 1.0.0-beta.2 | -| 2.0.x | 1.0.0 | +| stac-pydantic | STAC Version | +|---------------|--------------| +| 1.1.x | 0.9.0 | +| 1.2.x | 1.0.0-beta.1 | +| 1.3.x | 1.0.0-beta.2 | +| 2.0.x | 1.0.0 | + +## Development + +Install the [pre-commit](https://pre-commit.com/) hooks: + +```shell +pre-commit install +``` ## Testing -Run the entire test suite: + +Ensure you have all Python versions installed that the tests will be run against. If using pyenv, run: + +```shell +pyenv install 3.7.12 +pyenv install 3.8.12 +pyenv install 3.9.10 +pyenv install 3.10.2 +pyenv local 3.7.12 3.8.12 3.9.10 3.10.2 ``` + +Run the entire test suite: + +```shell tox ``` Run a single test case using the standard pytest convention: -``` + +```shell pytest -v tests/test_models.py::test_item_extensions ``` ## Usage + ### Loading Models + Load data into models with standard pydantic: + ```python from stac_pydantic import Catalog stac_catalog = { - "stac_version": "0.9.0", + "type": "Catalog", + "stac_version": "1.0.0", "id": "sample", "description": "This is a very basic sample catalog.", "links": [ diff --git a/setup.cfg b/setup.cfg index 921a2cd..4241d77 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,3 +3,6 @@ test=pytest [tool:pytest] addopts=-sv --cov=stac_pydantic --cov-fail-under=95 --cov-report=term-missing + +[mypy] +plugins = pydantic.mypy \ No newline at end of file diff --git a/setup.py b/setup.py index cd19ac8..4b30549 100644 --- a/setup.py +++ b/setup.py @@ -12,6 +12,8 @@ "shapely", "dictdiffer", "jsonschema", + "types-requests", + "types-jsonschema", ], } @@ -28,6 +30,8 @@ "Intended Audience :: Science/Research", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", "License :: OSI Approved :: MIT License", ], keywords="stac pydantic validation", diff --git a/stac_pydantic/__init__.py b/stac_pydantic/__init__.py index 2c67cf2..872c09d 100644 --- a/stac_pydantic/__init__.py +++ b/stac_pydantic/__init__.py @@ -1,3 +1,4 @@ +# flake8: noqa: F401 from .catalog import Catalog from .collection import Collection from .item import Item, ItemCollection, ItemProperties diff --git a/stac_pydantic/api/__init__.py b/stac_pydantic/api/__init__.py index a3321f0..e8b40a2 100644 --- a/stac_pydantic/api/__init__.py +++ b/stac_pydantic/api/__init__.py @@ -1,3 +1,4 @@ +# flake8: noqa: F401 from .collections import Collections from .conformance import ConformanceClasses from .landing import LandingPage diff --git a/stac_pydantic/api/collections.py b/stac_pydantic/api/collections.py index d15feba..d0c270d 100644 --- a/stac_pydantic/api/collections.py +++ b/stac_pydantic/api/collections.py @@ -1,4 +1,4 @@ -from typing import List +from typing import Any, Dict, List from pydantic import BaseModel @@ -14,8 +14,8 @@ class Collections(BaseModel): links: List[Link] collections: List[Collection] - def to_dict(self, **kwargs): + def to_dict(self, **kwargs: Any) -> Dict[str, Any]: return self.dict(by_alias=True, exclude_unset=True, **kwargs) - def to_json(self, **kwargs): + def to_json(self, **kwargs: Any) -> str: return self.json(by_alias=True, exclude_unset=True, **kwargs) diff --git a/stac_pydantic/api/extensions/context.py b/stac_pydantic/api/extensions/context.py index 44b46e8..bcda790 100644 --- a/stac_pydantic/api/extensions/context.py +++ b/stac_pydantic/api/extensions/context.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Any, Dict, Optional from pydantic import BaseModel, validator @@ -13,7 +13,7 @@ class ContextExtension(BaseModel): matched: Optional[int] @validator("limit") - def validate_limit(cls, v, values): + def validate_limit(cls, v: Optional[int], values: Dict[str, Any]) -> None: if values["returned"] > v: raise ValueError( "Number of returned items must be less than or equal to the limit" diff --git a/stac_pydantic/api/extensions/fields.py b/stac_pydantic/api/extensions/fields.py index 5425351..7027252 100644 --- a/stac_pydantic/api/extensions/fields.py +++ b/stac_pydantic/api/extensions/fields.py @@ -11,7 +11,7 @@ class FieldsExtension(BaseModel): includes: Optional[Set[str]] excludes: Optional[Set[str]] - def _get_field_dict(self, fields: Set[str]) -> Dict: + 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 +29,7 @@ def _get_field_dict(self, fields: Set[str]) -> Dict: return field_dict @property - def filter(self) -> Dict: + 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 +37,7 @@ def filter(self) -> Dict: Ref: https://pydantic-docs.helpmanual.io/usage/exporting_models/#advanced-include-and-exclude """ - include = 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 +46,5 @@ def filter(self) -> Dict: include = include.union(self.includes) - self.excludes return { "include": self._get_field_dict(include), - "exclude": self._get_field_dict(self.excludes), + "exclude": self._get_field_dict(self.excludes or Set()), } diff --git a/stac_pydantic/api/extensions/sort.py b/stac_pydantic/api/extensions/sort.py index 2ff76a4..8051fb3 100644 --- a/stac_pydantic/api/extensions/sort.py +++ b/stac_pydantic/api/extensions/sort.py @@ -1,6 +1,6 @@ from enum import auto -from pydantic import BaseModel, constr +from pydantic import BaseModel, Field from stac_pydantic.utils import AutoValueEnum @@ -15,5 +15,5 @@ class SortExtension(BaseModel): https://github.com/radiantearth/stac-api-spec/tree/master/extensions/sort#sort-api-extension """ - field: constr(min_length=1) + field: str = Field(..., alias="field", min_length=1) direction: SortDirections diff --git a/stac_pydantic/api/landing.py b/stac_pydantic/api/landing.py index f866273..616eadf 100644 --- a/stac_pydantic/api/landing.py +++ b/stac_pydantic/api/landing.py @@ -1,6 +1,6 @@ -from typing import List, Optional, Union +from typing import List, Optional -from pydantic import AnyUrl, BaseModel, Field, constr +from pydantic import AnyUrl, BaseModel, Field from stac_pydantic.links import Links from stac_pydantic.version import STAC_VERSION @@ -11,11 +11,11 @@ class LandingPage(BaseModel): https://github.com/radiantearth/stac-api-spec/blob/master/api-spec.md#ogc-api---features-endpoints """ - id: constr(min_length=1) - description: constr(min_length=1) + id: str = Field(..., alias="id", min_length=1) + description: str = Field(..., alias="description", min_length=1) title: Optional[str] stac_version: str = Field(STAC_VERSION, const=True) stac_extensions: Optional[List[AnyUrl]] conformsTo: List[AnyUrl] links: Links - type: constr(min_length=1) = Field("Catalog", const=True) + type: str = Field("Catalog", const=True, min_length=1) diff --git a/stac_pydantic/api/search.py b/stac_pydantic/api/search.py index 4079d7a..cccd046 100644 --- a/stac_pydantic/api/search.py +++ b/stac_pydantic/api/search.py @@ -1,7 +1,7 @@ -from datetime import datetime -from typing import Any, Dict, List, Optional, Union +from datetime import datetime as dt +from typing import Any, Dict, List, Optional, Tuple, Union, cast -from geojson_pydantic.geometries import ( +from geojson_pydantic.geometries import ( # type: ignore GeometryCollection, LineString, MultiLineString, @@ -19,6 +19,16 @@ from stac_pydantic.api.extensions.sort import SortExtension from stac_pydantic.shared import BBox +Intersection = Union[ + Point, + MultiPoint, + LineString, + MultiLineString, + Polygon, + MultiPolygon, + GeometryCollection, +] + class Search(BaseModel): """ @@ -30,23 +40,13 @@ class Search(BaseModel): collections: Optional[List[str]] ids: Optional[List[str]] bbox: Optional[BBox] - intersects: Optional[ - Union[ - Point, - MultiPoint, - LineString, - MultiLineString, - Polygon, - MultiPolygon, - GeometryCollection, - ] - ] + intersects: Optional[Intersection] datetime: Optional[str] limit: int = 10 @property - def start_date(self) -> Optional[datetime]: - values = self.datetime.split("/") + def start_date(self) -> Optional[dt]: + values = (self.datetime or "").split("/") if len(values) == 1: return None if values[0] == ".." or values[0] == "": @@ -54,8 +54,8 @@ def start_date(self) -> Optional[datetime]: return parse_datetime(values[0]) @property - def end_date(self) -> Optional[datetime]: - values = self.datetime.split("/") + def end_date(self) -> Optional[dt]: + values = (self.datetime or "").split("/") if len(values) == 1: return parse_datetime(values[0]) if values[1] == ".." or values[1] == "": @@ -63,19 +63,25 @@ def end_date(self) -> Optional[datetime]: return parse_datetime(values[1]) @validator("intersects") - def validate_spatial(cls, v, values): - if v and values["bbox"]: + def validate_spatial( + cls, + v: Intersection, + values: Dict[str, Any], + ) -> Intersection: + if v and values["bbox"] is not None: raise ValueError("intersects and bbox parameters are mutually exclusive") return v @validator("bbox") - def validate_bbox(cls, v: BBox): + def validate_bbox(cls, v: BBox) -> BBox: if v: # Validate order if len(v) == 4: - xmin, ymin, xmax, ymax = v + xmin, ymin, xmax, ymax = cast(Tuple[int, int, int, int], v) else: - xmin, ymin, min_elev, xmax, ymax, max_elev = v + xmin, ymin, min_elev, xmax, ymax, max_elev = cast( + Tuple[int, int, int, int, int, int], v + ) if max_elev < min_elev: raise ValueError( "Maximum elevation must greater than minimum elevation" @@ -98,7 +104,7 @@ def validate_bbox(cls, v: BBox): return v @validator("datetime") - def validate_datetime(cls, v): + def validate_datetime(cls, v: str) -> str: if "/" in v: values = v.split("/") else: @@ -129,20 +135,11 @@ def spatial_filter(self) -> Optional[_GeometryBase]: Check for both because the ``bbox`` and ``intersects`` parameters are mutually exclusive. """ if self.bbox: - return Polygon( - coordinates=[ - [ - [self.bbox[0], self.bbox[3]], - [self.bbox[2], self.bbox[3]], - [self.bbox[2], self.bbox[1]], - [self.bbox[0], self.bbox[1]], - [self.bbox[0], self.bbox[3]], - ] - ] - ) + return Polygon.from_bounds(*self.bbox) if self.intersects: return self.intersects - return + else: + return None class ExtendedSearch(Search): diff --git a/stac_pydantic/api/utils/link_factory.py b/stac_pydantic/api/utils/link_factory.py index 1750e26..90d341b 100644 --- a/stac_pydantic/api/utils/link_factory.py +++ b/stac_pydantic/api/utils/link_factory.py @@ -11,7 +11,7 @@ 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.""" @@ -31,7 +31,7 @@ class CollectionLinks(BaseLinks): """Create inferred links specific to collections.""" collection_id: str - _link_members: ClassVar[Tuple[str]] = ("root", "self", "parent", "item") + _link_members: ClassVar[Tuple[str, ...]] = ("root", "self", "parent", "item") def self(self) -> Link: """Create the `self` link.""" @@ -62,7 +62,7 @@ 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.""" diff --git a/stac_pydantic/catalog.py b/stac_pydantic/catalog.py index 8d01c17..67eb0fc 100644 --- a/stac_pydantic/catalog.py +++ b/stac_pydantic/catalog.py @@ -1,6 +1,6 @@ -from typing import List, Optional +from typing import Any, Dict, List, Optional -from pydantic import AnyUrl, BaseModel, Field, constr, root_validator +from pydantic import AnyUrl, BaseModel, Field from stac_pydantic.links import Links from stac_pydantic.version import STAC_VERSION @@ -11,20 +11,20 @@ class Catalog(BaseModel): https://github.com/radiantearth/stac-spec/blob/v1.0.0/catalog-spec/catalog-spec.md """ - id: constr(min_length=1) - description: constr(min_length=1) - stac_version: constr(min_length=1) = Field(STAC_VERSION, const=True) + id: str = Field(..., alias="", min_length=1) + description: str = Field(..., alias="description", min_length=1) + stac_version: str = Field(STAC_VERSION, const=True, min_length=1) links: Links stac_extensions: Optional[List[AnyUrl]] title: Optional[str] - type: constr(min_length=1) = Field("Catalog", const=True) + type: str = Field("Catalog", const=True, min_length=1) class Config: use_enum_values = True extra = "allow" - def to_dict(self, **kwargs): + def to_dict(self: "Catalog", **kwargs: Any) -> Dict[str, Any]: return self.dict(by_alias=True, exclude_unset=True, **kwargs) - def to_json(self, **kwargs): + def to_json(self: "Catalog", **kwargs: Any) -> str: return self.json(by_alias=True, exclude_unset=True, **kwargs) diff --git a/stac_pydantic/collection.py b/stac_pydantic/collection.py index b59321b..980f619 100644 --- a/stac_pydantic/collection.py +++ b/stac_pydantic/collection.py @@ -1,6 +1,6 @@ from typing import Any, Dict, List, Optional, Union -from pydantic import BaseModel, Field, constr +from pydantic import BaseModel, Field from stac_pydantic.catalog import Catalog from stac_pydantic.shared import Asset, NumType, Provider @@ -46,10 +46,10 @@ class Collection(Catalog): """ assets: Optional[Dict[str, Asset]] - license: constr(min_length=1) + license: str = Field(..., alias="license", min_length=1) extent: Extent title: Optional[str] keywords: Optional[List[str]] providers: Optional[List[Provider]] summaries: Optional[Dict[str, Union[Range, List[Any], Dict[str, Any]]]] - type: constr(min_length=1) = Field("Collection", const=True) + type: str = Field("Collection", const=True, min_length=1) diff --git a/stac_pydantic/extensions.py b/stac_pydantic/extensions.py index f808b3c..ecdb6ba 100644 --- a/stac_pydantic/extensions.py +++ b/stac_pydantic/extensions.py @@ -1,4 +1,4 @@ -from functools import singledispatch +from typing import Union import jsonschema import requests @@ -6,36 +6,22 @@ from stac_pydantic import Catalog, Collection, Item -@singledispatch -def validate_extensions(stac_obj, b): - raise NotImplementedError("Unsupported type") +def validate_extensions( + stac_obj: Union[Item, Collection, Catalog, dict], reraise_exception: bool = False +) -> bool: + if isinstance(stac_obj, dict): + stac_dict = stac_obj + else: + stac_dict = stac_obj.dict() - -@validate_extensions.register -def _(stac_obj: dict, reraise_exception: bool = False, **kwargs) -> bool: try: - if stac_obj["stac_extensions"]: - for ext in stac_obj["stac_extensions"]: + if stac_dict["stac_extensions"]: + for ext in stac_dict["stac_extensions"]: req = requests.get(ext) schema = req.json() - jsonschema.validate(instance=stac_obj, schema=schema) + jsonschema.validate(instance=stac_dict, schema=schema) except Exception: if reraise_exception: raise return False return True - - -@validate_extensions.register -def _(stac_obj: Item, reraise_exception: bool = False, **kwargs) -> bool: - validate_extensions(stac_obj.dict(), reraise_exception) - - -@validate_extensions.register -def _(stac_obj: Collection, reraise_exception: bool = False, **kwargs) -> bool: - validate_extensions(stac_obj.dict(), reraise_exception) - - -@validate_extensions.register -def _(stac_obj: Catalog, reraise_exception: bool = False, **kwargs) -> bool: - validate_extensions(stac_obj.dict(), reraise_exception) diff --git a/stac_pydantic/item.py b/stac_pydantic/item.py index 658076f..8852ea6 100644 --- a/stac_pydantic/item.py +++ b/stac_pydantic/item.py @@ -1,13 +1,13 @@ from datetime import datetime as dt -from typing import Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, Union -from geojson_pydantic.features import Feature, FeatureCollection -from pydantic import AnyUrl, Field, constr, root_validator, validator +from geojson_pydantic.features import Feature, FeatureCollection # type: ignore +from pydantic import AnyUrl, Field, root_validator, validator from pydantic.datetime_parse import parse_datetime from stac_pydantic.api.extensions.context import ContextExtension from stac_pydantic.links import Links -from stac_pydantic.shared import DATETIME_RFC339, Asset, BBox, StacCommonMetadata +from stac_pydantic.shared import DATETIME_RFC339, Asset, StacCommonMetadata from stac_pydantic.version import STAC_VERSION @@ -19,7 +19,7 @@ class ItemProperties(StacCommonMetadata): datetime: Union[dt, str] = Field(..., alias="datetime") @validator("datetime") - def validate_datetime(cls, v, values): + def validate_datetime(cls, v: Union[dt, str], values: Dict[str, Any]) -> dt: if v == "null": if not values["start_datetime"] and not values["end_datetime"]: raise ValueError( @@ -36,45 +36,45 @@ class Config: json_encoders = {dt: lambda v: v.strftime(DATETIME_RFC339)} -class Item(Feature): +class Item(Feature): # type: ignore """ https://github.com/radiantearth/stac-spec/blob/v1.0.0/item-spec/item-spec.md """ - id: constr(min_length=1) - stac_version: constr(min_length=1) = Field(STAC_VERSION, const=True) + id: str = Field(..., alias="id", min_length=1) + stac_version: str = Field(STAC_VERSION, const=True, min_length=1) properties: ItemProperties assets: Dict[str, Asset] links: Links stac_extensions: Optional[List[AnyUrl]] collection: Optional[str] - def to_dict(self, **kwargs): - return self.dict(by_alias=True, exclude_unset=True, **kwargs) + def to_dict(self, **kwargs: Any) -> Dict[str, Any]: + return self.dict(by_alias=True, exclude_unset=True, **kwargs) # type: ignore - def to_json(self, **kwargs): - return self.json(by_alias=True, exclude_unset=True, **kwargs) + def to_json(self, **kwargs: Any) -> str: + return self.json(by_alias=True, exclude_unset=True, **kwargs) # type: ignore @root_validator(pre=True) - def validate_bbox(cls, values): + def validate_bbox(cls, values: Dict[str, Any]) -> Dict[str, Any]: if values.get("geometry") and values.get("bbox") is None: raise ValueError("bbox is required if geometry is not null") return values -class ItemCollection(FeatureCollection): +class ItemCollection(FeatureCollection): # type: ignore """ https://github.com/radiantearth/stac-spec/blob/v1.0.0/item-spec/itemcollection-spec.md """ - stac_version: constr(min_length=1) = Field(STAC_VERSION, const=True) + stac_version: str = Field(STAC_VERSION, const=True, min_length=1) features: List[Item] stac_extensions: Optional[List[AnyUrl]] links: Links context: Optional[ContextExtension] - def to_dict(self, **kwargs): - return self.dict(by_alias=True, exclude_unset=True, **kwargs) + def to_dict(self, **kwargs: Any) -> Dict[str, Any]: + return self.dict(by_alias=True, exclude_unset=True, **kwargs) # type: ignore - def to_json(self, **kwargs): - return self.json(by_alias=True, exclude_unset=True, **kwargs) + def to_json(self, **kwargs: Any) -> str: + return self.json(by_alias=True, exclude_unset=True, **kwargs) # type: ignore diff --git a/stac_pydantic/links.py b/stac_pydantic/links.py index 7d52343..f847141 100644 --- a/stac_pydantic/links.py +++ b/stac_pydantic/links.py @@ -2,7 +2,7 @@ from typing import Any, Dict, Iterator, List, Optional, Union from urllib.parse import urljoin -from pydantic import BaseModel, Field, constr +from pydantic import BaseModel, Field from stac_pydantic.utils import AutoValueEnum @@ -30,8 +30,8 @@ class Link(BaseModel): https://github.com/radiantearth/stac-spec/blob/v1.0.0/collection-spec/collection-spec.md#link-object """ - href: constr(min_length=1) - rel: constr(min_length=1) + href: str = Field(..., alias="href", min_length=1) + rel: str = Field(..., alias="rel", min_length=1) type: Optional[str] title: Optional[str] # Label extension @@ -40,7 +40,7 @@ class Link(BaseModel): class Config: use_enum_values = True - def resolve(self, base_url: str): + def resolve(self, base_url: str) -> None: """resolve a link to the given base URL""" self.href = urljoin(base_url, self.href) @@ -59,22 +59,22 @@ class PaginationLink(Link): class Links(BaseModel): __root__: List[Union[PaginationLink, Link]] - def resolve(self, base_url: str): + def link_iterator(self) -> Iterator[Link]: + """Produce iterator to iterate through links""" + return iter(self.__root__) + + def resolve(self, base_url: str) -> None: """resolve all links to the given base URL""" - for link in self: + for link in self.link_iterator(): link.resolve(base_url) - def append(self, link: Link): + def append(self, link: Link) -> None: self.__root__.append(link) - def __iter__(self) -> Iterator[Link]: - """iterate through links""" - return iter(self.__root__) - - def __len__(self): + def __len__(self) -> int: return len(self.__root__) - def __getitem__(self, idx): + def __getitem__(self, idx: int) -> Union[PaginationLink, Link]: return self.__root__[idx] diff --git a/stac_pydantic/scripts/cli.py b/stac_pydantic/scripts/cli.py index 7b891fc..a850f4f 100644 --- a/stac_pydantic/scripts/cli.py +++ b/stac_pydantic/scripts/cli.py @@ -2,19 +2,19 @@ import requests from pydantic import ValidationError -from stac_pydantic import Catalog, Collection, Item +from stac_pydantic import Item from stac_pydantic.extensions import validate_extensions @click.group(short_help="Validate STAC") -def app(): +def app() -> None: """stac-pydantic cli group""" pass @app.command(short_help="Validate STAC Item") @click.argument("infile") -def validate_item(infile): +def validate_item(infile: str) -> None: """Validate stac item""" r = requests.get(infile) r.raise_for_status() diff --git a/stac_pydantic/shared.py b/stac_pydantic/shared.py index 6630b73..a1f0b79 100644 --- a/stac_pydantic/shared.py +++ b/stac_pydantic/shared.py @@ -2,7 +2,7 @@ from enum import Enum, auto from typing import List, Optional, Tuple, Union -from pydantic import BaseModel, Extra, Field, confloat, constr +from pydantic import BaseModel, Extra, Field from stac_pydantic.utils import AutoValueEnum @@ -66,7 +66,7 @@ class Provider(BaseModel): https://github.com/radiantearth/stac-spec/blob/v1.0.0/collection-spec/collection-spec.md#provider-object """ - name: constr(min_length=1) + name: str = Field(..., alias="name", min_length=1) description: Optional[str] roles: Optional[List[str]] url: Optional[str] @@ -88,7 +88,7 @@ class StacCommonMetadata(BaseModel): constellation: Optional[str] = Field(None, alias="constellation") mission: Optional[str] = Field(None, alias="mission") providers: Optional[List[Provider]] = Field(None, alias="providers") - gsd: Optional[confloat(gt=0)] = Field(None, alias="gsd") + gsd: Optional[float] = Field(None, alias="gsd", gt=0) class Config: json_encoders = {datetime: lambda v: v.strftime(DATETIME_RFC339)} @@ -99,7 +99,7 @@ class Asset(StacCommonMetadata): https://github.com/radiantearth/stac-spec/blob/v1.0.0/item-spec/item-spec.md#asset-object """ - href: constr(min_length=1) + href: str = Field(..., alias="href", min_length=1) type: Optional[str] title: Optional[str] description: Optional[str] diff --git a/stac_pydantic/utils.py b/stac_pydantic/utils.py index adeda2f..1bdca60 100644 --- a/stac_pydantic/utils.py +++ b/stac_pydantic/utils.py @@ -1,9 +1,9 @@ from enum import Enum -from typing import Dict, Optional, Type - -from pydantic import BaseModel +from typing import Any, List class AutoValueEnum(Enum): - def _generate_next_value_(name, start, count, last_values): + def _generate_next_value_( # type: ignore + name: str, start: int, count: int, last_values: List[Any] + ) -> Any: return name diff --git a/tests/conftest.py b/tests/conftest.py index b7a3a9d..912cece 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,10 +1,9 @@ import json import operator import os -from datetime import datetime, timedelta import arrow -import dictdiffer +import dictdiffer # type: ignore import pytest import requests from click.testing import CliRunner diff --git a/tests/test_api_extensions.py b/tests/test_api_extensions.py index 4a3e815..014633d 100644 --- a/tests/test_api_extensions.py +++ b/tests/test_api_extensions.py @@ -2,7 +2,7 @@ import pytest from pydantic import ValidationError -from shapely.geometry import Polygon, shape +from shapely.geometry import Polygon, shape # type: ignore from stac_pydantic import Item from stac_pydantic.api.extensions.fields import FieldsExtension diff --git a/tests/test_link_factory.py b/tests/test_link_factory.py index 97e8e77..a1865e4 100644 --- a/tests/test_link_factory.py +++ b/tests/test_link_factory.py @@ -9,7 +9,7 @@ def test_collection_links(): links = CollectionLinks( collection_id="collection", base_url="http://stac.com" ).create_links() - for link in links: + for link in links.link_iterator(): assert isinstance(link, Link) assert link.rel in CollectionLinks._link_members @@ -18,7 +18,7 @@ def test_item_links(): links = ItemLinks( collection_id="collection", item_id="item", base_url="http://stac.com" ).create_links() - for link in links: + for link in links.link_iterator(): assert isinstance(link, Link) assert link.rel in ItemLinks._link_members diff --git a/tests/test_models.py b/tests/test_models.py index 5e85610..8c27fc6 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -4,7 +4,7 @@ import pytest from pydantic import Field, ValidationError -from shapely.geometry import shape +from shapely.geometry import shape # type: ignore from stac_pydantic import Catalog, Collection, Item, ItemCollection, ItemProperties from stac_pydantic.api import Collections @@ -22,8 +22,10 @@ ITEM_COLLECTION = "itemcollection-sample-full.json" SINGLE_FILE_STAC = "example-search.json" -# ASSET_EXTENSION = f"https://raw.githubusercontent.com/radiantearth/stac-spec/v{STAC_VERSION}/extensions/asset/examples/example-landsat8.json" -# COLLECTION_ASSET_EXTENSION = f"https://raw.githubusercontent.com/radiantearth/stac-spec/v{STAC_VERSION}/extensions/collection-assets/examples/example-esm.json" +# ASSET_EXTENSION = f"https://raw.githubusercontent.com/radiantearth/stac-spec/v{STAC_VERSION}/extensions +# /asset/examples/example-landsat8.json" +# COLLECTION_ASSET_EXTENSION = f"https://raw.githubusercontent.com/radiantearth/stac-spec/v{STAC_VERSION} +# /extensions/collection-assets/examples/example-esm.json" DATACUBE_EXTENSION = "example-item_datacube-extension.json" EO_EXTENSION = "example-landsat8_eo-extension.json" ITEM_ASSET_EXTENSION = "example-landsat8_item-assets-extension.json" @@ -159,7 +161,7 @@ def test_invalid_geometry(): # Remove the last coordinate test_item["geometry"]["coordinates"][0].pop(-1) - with pytest.raises(ValidationError) as e: + with pytest.raises(ValidationError): Item(**test_item) @@ -232,7 +234,7 @@ def test_api_landing_page_is_catalog(): ) ], ) - catalog = Catalog(**landing_page.dict()) + Catalog(**landing_page.dict()) def test_search(): @@ -286,7 +288,7 @@ def test_temporal_search_single_tailed(): utcnow = datetime.utcnow().replace(microsecond=0, tzinfo=timezone.utc) utcnow_str = utcnow.strftime(DATETIME_RFC339) search = Search(collections=["collection1"], datetime=utcnow_str) - assert search.start_date == None + assert search.start_date is None assert search.end_date == utcnow @@ -299,31 +301,32 @@ def test_temporal_search_two_tailed(): search = Search(collections=["collection1"], datetime=f"{utcnow_str}/..") assert search.start_date == utcnow - assert search.end_date == None + assert search.end_date is None search = Search(collections=["collection1"], datetime=f"{utcnow_str}/") assert search.start_date == utcnow - assert search.end_date == None + assert search.end_date is None def test_temporal_search_open(): # Test open date range search = Search(collections=["collection1"], datetime="../..") - assert search.start_date == search.end_date == None + assert search.start_date is None + assert search.end_date is None def test_invalid_temporal_search(): # Not RFC339 utcnow = datetime.utcnow().strftime("%Y-%m-%d") with pytest.raises(ValidationError): - search = Search(collections=["collection1"], datetime=utcnow) + Search(collections=["collection1"], datetime=utcnow) # End date is before start date start = datetime.utcnow() time.sleep(2) end = datetime.utcnow() with pytest.raises(ValidationError): - search = Search( + Search( collections=["collection1"], datetime=f"{end.strftime(DATETIME_RFC339)}/{start.strftime(DATETIME_RFC339)}", ) @@ -442,7 +445,7 @@ class TestProperties(ItemProperties): class Config: allow_population_by_fieldname = True - alias_generator = lambda field_name: f"test:{field_name}" + alias_generator = lambda field_name: f"test:{field_name}" # noqa: E731 class TestItem(Item): properties: TestProperties @@ -525,7 +528,7 @@ def test_resolve_link(): def test_resolve_links(): links = Links.parse_obj([Link(href="/hello/world", type="image/jpeg", rel="test")]) links.resolve(base_url="http://base_url.com") - for link in links: + for link in links.link_iterator(): assert link.href == "http://base_url.com/hello/world" @@ -536,7 +539,7 @@ def test_resolve_pagination_link(): ) links = Links.parse_obj([normal_link, page_link]) links.resolve(base_url="http://base_url.com") - for link in links: + for link in links.link_iterator(): if isinstance(link, PaginationLink): assert link.href == "http://base_url.com/next/page" @@ -556,7 +559,5 @@ def test_geometry_null_item(): def test_item_bbox_validation(): test_item = request(LABEL_EXTENSION) test_item["bbox"] = None - with pytest.raises( - ValueError, match="bbox is required if geometry is not null" - ) as e: + with pytest.raises(ValueError, match="bbox is required if geometry is not null"): Item(**test_item) diff --git a/tox.ini b/tox.ini index 27ad836..f0bb71d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,21 +1,25 @@ [tox] -envlist = py37,py38 +envlist = py37,py38,py39,py310 [testenv] extras = dev -commands= - python -m pytest --cov stac_pydantic --cov-report xml --cov-report term-missing -deps= - numpy +commands = python -m pytest --cov stac_pydantic --cov-report xml --cov-report term-missing + +[black] +deps = black +commands = black . -# Lint [flake8] -exclude = .git,__pycache__,docs/source/conf.py,old,build,dist -max-line-length = 90 +deps = flake8 +exclude = .git,__pycache__,docs/source/conf.py,old,build,dist,.venv,.tox,.idea +max-line-length = 88 +select = C,E,F,W,B,B950 +extend-ignore = E203, E501 +commands = flake8 . [mypy] -no_strict_optional = True -ignore_missing_imports = True +deps = mypy, types-click +commands = mypy --no_strict_optional --ignore_missing_imports --implicit_reexport stac_pydantic/ [tool:isort] profile = black