From 07d2059be0b487901a9dc015eeff5391ae55c485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi?= <remi.cresson@inrae.fr> Date: Tue, 28 Jan 2025 16:43:02 +0100 Subject: [PATCH 1/2] lint, QTY, etc --- .gitlab-ci.yml | 2 +- pyproject.toml | 17 ++++ stac_extension_genmeta/__init__.py | 5 +- stac_extension_genmeta/core.py | 71 ++++++++-------- stac_extension_genmeta/schema.py | 130 ++++++++--------------------- stac_extension_genmeta/testing.py | 75 ++++++++--------- tests/extensions_test.py | 15 ++-- 7 files changed, 135 insertions(+), 180 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4bb2302..4038e12 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,6 +16,6 @@ Tests: except: [main, tags] script: - pip install pip --upgrade - - pip install . + - pip install .[tests] - python3 tests/extensions_test.py diff --git a/pyproject.toml b/pyproject.toml index c4b3d8d..0ad4e6c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ packages = ["stac_extension_genmeta"] [project.optional-dependencies] validation = ["requests", "pystac[validation]"] +tests = ["requests"] [tool.setuptools.dynamic] version = { attr = "stac_extension_genmeta.__version__" } @@ -35,3 +36,19 @@ version = { attr = "stac_extension_genmeta.__version__" } [tool.pydocstyle] convention = "google" +[tool.mypy] +plugins = [ + "pydantic.mypy" +] +show_error_codes = true +pretty = true +exclude = ["doc", "venv", ".venv"] + +[tool.pydantic-mypy] +init_forbid_extra = true +init_typed = true +warn_required_dynamic_aliases = true +warn_untyped_fields = true + +[tool.pylint] +disable = "W1203,R0903,E0401,W0622,C0116,C0115,W0719" diff --git a/stac_extension_genmeta/__init__.py b/stac_extension_genmeta/__init__.py index eea0c50..adcf023 100644 --- a/stac_extension_genmeta/__init__.py +++ b/stac_extension_genmeta/__init__.py @@ -1,2 +1,5 @@ -from .core import create_extension_cls +"""STAC extension generic metadata.""" + +from .core import create_extension_cls # noqa + __version__ = "0.1.2" diff --git a/stac_extension_genmeta/core.py b/stac_extension_genmeta/core.py index a63c962..d9a9333 100644 --- a/stac_extension_genmeta/core.py +++ b/stac_extension_genmeta/core.py @@ -1,23 +1,19 @@ """ Processing extension """ -from typing import Any, Generic, TypeVar, Union, cast -from pystac.extensions.base import PropertiesExtension, \ - ExtensionManagementMixin -import pystac -from pydantic import BaseModel, Field + import re +from typing import Any, Generic, TypeVar, Union, cast from collections.abc import Iterable -from .schema import generate_schema import json +from pystac.extensions.base import PropertiesExtension, ExtensionManagementMixin +import pystac +from pydantic import BaseModel +from .schema import generate_schema -def create_extension_cls( - model_cls: BaseModel, - schema_uri: str -) -> PropertiesExtension: - """ - This method creates a pystac extension from a pydantic model. +def create_extension_cls(model_cls: BaseModel, schema_uri: str) -> PropertiesExtension: + """This method creates a pystac extension from a pydantic model. Args: model_cls: pydantic model class @@ -40,34 +36,36 @@ def create_extension_cls( class CustomExtension( Generic[T], PropertiesExtension, - ExtensionManagementMixin[Union[pystac.Item, pystac.Collection]] + ExtensionManagementMixin[Union[pystac.Item, pystac.Collection]], ): + """Custom extension Class.""" + def __init__(self, obj: T): + """Initializer.""" if isinstance(obj, pystac.Item): self.properties = obj.properties elif isinstance(obj, (pystac.Asset, pystac.Collection)): self.properties = obj.extra_fields else: raise pystac.ExtensionTypeError( - f"{model_cls.__name__} cannot be instantiated from type " - f"{type(obj).__name__}" + f"{model_cls.__name__} cannot be instantiated from type {type(obj).__name__}" ) # Try to get properties from STAC item # If not possible, self.md is set to `None` props = { - key: self._get_property(info.alias, str) + key: self._get_property(str(info.alias), str) for key, info in model_cls.__fields__.items() } props = {p: v for p, v in props.items() if v is not None} self.md = model_cls(**props) if props else None def __getattr__(self, item): - # forward getattr to self.md + """Forward getattr to `self.md`.""" return getattr(self.md, item) if self.md else None def apply(self, md: model_cls = None, **kwargs) -> None: - + """Apply some metadata to the STAC object.""" if md is None and not kwargs: raise ValueError("At least `md` or kwargs is required") @@ -85,20 +83,22 @@ def create_extension_cls( @classmethod def get_schema_uri(cls) -> str: + """Return the schema URL.""" return schema_uri @classmethod def get_schema(cls) -> dict: + """Return the shema as a dict.""" return generate_schema( model_cls=model_cls, title=f"STAC extension from {model_cls.__name__} model", - description=f"STAC extension based on the {model_cls.__name__} " - "model", - schema_uri=schema_uri + description=f"STAC extension based on the {model_cls.__name__} model", + schema_uri=schema_uri, ) @classmethod def print_schema(cls): + """Display the schema.""" print( "\033[92mPlease copy/paste the schema below in the right place " f"in the repository so it can be accessed from \033[94m" @@ -107,47 +107,50 @@ def create_extension_cls( @classmethod def export_schema(cls, json_file): - with open(json_file, 'w') as f: + """Export the schema.""" + with open(json_file, "w", encoding="utf-8") as f: json.dump(cls.get_schema(), f, indent=2) @classmethod - def ext( - cls, - obj: T, - add_if_missing: bool = False - ) -> model_cls.__name__: + def ext(cls, obj: T, add_if_missing: bool = False) -> model_cls.__name__: + """Instanciate an extension.""" if isinstance(obj, pystac.Item): cls.ensure_has_extension(obj, add_if_missing) return cast(CustomExtension[T], ItemCustomExtension(obj)) - elif isinstance(obj, pystac.Asset): + if isinstance(obj, pystac.Asset): cls.ensure_owner_has_extension(obj, add_if_missing) return cast(CustomExtension[T], AssetCustomExtension(obj)) - elif isinstance(obj, pystac.Collection): + if isinstance(obj, pystac.Collection): cls.ensure_has_extension(obj, add_if_missing) return cast(CustomExtension[T], CollectionCustomExtension(obj)) raise pystac.ExtensionTypeError( - f"{model_cls.__name__} does not apply to type " - f"{type(obj).__name__}" + f"{model_cls.__name__} does not apply to type {type(obj).__name__}" ) class ItemCustomExtension(CustomExtension[pystac.Item]): - pass + """Item custom extension.""" class AssetCustomExtension(CustomExtension[pystac.Asset]): + """Asset custom extension.""" + asset_href: str properties: dict[str, Any] additional_read_properties: Iterable[dict[str, Any]] | None = None - def __init__(self, asset: pystac.Asset): + def __init__(self, asset: pystac.Asset): # pylint: disable=super-init-not-called + """Initializer.""" self.asset_href = asset.href self.properties = asset.extra_fields if asset.owner and isinstance(asset.owner, pystac.Item): self.additional_read_properties = [asset.owner.properties] class CollectionCustomExtension(CustomExtension[pystac.Collection]): + """Collection custom extension.""" + properties: dict[str, Any] - def __init__(self, collection: pystac.Collection): + def __init__(self, collection: pystac.Collection): # pylint: disable=super-init-not-called + """Initializer.""" self.properties = collection.extra_fields CustomExtension.__name__ = f"CustomExtensionFrom{model_cls.__name__}" diff --git a/stac_extension_genmeta/schema.py b/stac_extension_genmeta/schema.py index ffe1cd7..adf35c4 100644 --- a/stac_extension_genmeta/schema.py +++ b/stac_extension_genmeta/schema.py @@ -1,12 +1,10 @@ +"""Schema generation.""" + from pydantic import BaseModel -def generate_schema( - model_cls: BaseModel, - title: str, - description: str, - schema_uri: str -) -> dict: +def generate_schema(model_cls: BaseModel, title: str, description: str, schema_uri: str) -> dict: + """Generate the schema from the model.""" properties = model_cls.model_json_schema() # prune "required" properties.pop("required", None) @@ -21,116 +19,60 @@ def generate_schema( "allOf": [ { "type": "object", - "required": [ - "type", - "properties", - "assets", - "links" - ], + "required": ["type", "properties", "assets", "links"], "properties": { - "type": { - "const": "Feature" - }, - "properties": { - "$ref": "#/definitions/fields" - }, - "assets": { - "$ref": "#/definitions/assets" - }, - "links": { - "$ref": "#/definitions/links" - } - } + "type": {"const": "Feature"}, + "properties": {"$ref": "#/definitions/fields"}, + "assets": {"$ref": "#/definitions/assets"}, + "links": {"$ref": "#/definitions/links"}, + }, }, - { - "$ref": "#/definitions/stac_extensions" - } - ] + {"$ref": "#/definitions/stac_extensions"}, + ], }, { "$comment": "This is the schema for STAC Collections.", "allOf": [ { "type": "object", - "required": [ - "type" - ], + "required": ["type"], "properties": { - "type": { - "const": "Collection" - }, - "assets": { - "$ref": "#/definitions/assets" - }, - "item_assets": { - "$ref": "#/definitions/assets" - }, - "links": { - "$ref": "#/definitions/links" - } - } - }, - { - "$ref": "#/definitions/fields" + "type": {"const": "Collection"}, + "assets": {"$ref": "#/definitions/assets"}, + "item_assets": {"$ref": "#/definitions/assets"}, + "links": {"$ref": "#/definitions/links"}, + }, }, - { - "$ref": "#/definitions/stac_extensions" - } - ] + {"$ref": "#/definitions/fields"}, + {"$ref": "#/definitions/stac_extensions"}, + ], }, { "$comment": "This is the schema for STAC Catalogs.", "allOf": [ { "type": "object", - "required": [ - "type" - ], + "required": ["type"], "properties": { - "type": { - "const": "Catalog" - }, - "links": { - "$ref": "#/definitions/links" - } - } - }, - { - "$ref": "#/definitions/fields" + "type": {"const": "Catalog"}, + "links": {"$ref": "#/definitions/links"}, + }, }, - { - "$ref": "#/definitions/stac_extensions" - } - ] - } + {"$ref": "#/definitions/fields"}, + {"$ref": "#/definitions/stac_extensions"}, + ], + }, ], "definitions": { "stac_extensions": { "type": "object", - "required": [ - "stac_extensions" - ], + "required": ["stac_extensions"], "properties": { - "stac_extensions": { - "type": "array", - "contains": { - "const": schema_uri - } - } - } - }, - "links": { - "type": "array", - "items": { - "$ref": "#/definitions/fields" - } - }, - "assets": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/fields" - } + "stac_extensions": {"type": "array", "contains": {"const": schema_uri}} + }, }, - "fields": properties - } + "links": {"type": "array", "items": {"$ref": "#/definitions/fields"}}, + "assets": {"type": "object", "additionalProperties": {"$ref": "#/definitions/fields"}}, + "fields": properties, + }, } diff --git a/stac_extension_genmeta/testing.py b/stac_extension_genmeta/testing.py index 5f51e2d..f0c294f 100644 --- a/stac_extension_genmeta/testing.py +++ b/stac_extension_genmeta/testing.py @@ -1,10 +1,15 @@ -import pystac -from datetime import datetime +"""Perform tests.""" + import random import json +import difflib +from datetime import datetime +import requests +import pystac def create_dummy_item(date=None): + """Create a dummy STAC item.""" if not date: date = datetime.now().replace(year=1999) @@ -15,21 +20,19 @@ def create_dummy_item(date=None): geom = { "type": "Polygon", "coordinates": [ - [[4.032730583418401, 43.547450099338604], - [4.036414917971517, 43.75162726634343], - [3.698685718905037, 43.75431706444037], - [3.6962018175925073, 43.55012996681564], - [4.032730583418401, 43.547450099338604]] - ] + [ + [4.032730583418401, 43.547450099338604], + [4.036414917971517, 43.75162726634343], + [3.698685718905037, 43.75431706444037], + [3.6962018175925073, 43.55012996681564], + [4.032730583418401, 43.547450099338604], + ] + ], } - asset = pystac.Asset( - href="https://example.com/SP67_FR_subset_1.tif" - ) + asset = pystac.Asset(href="https://example.com/SP67_FR_subset_1.tif") val = f"item_{random.uniform(10000, 80000)}" spat_extent = pystac.SpatialExtent([[0, 0, 2, 3]]) - temp_extent = pystac.TemporalExtent( - intervals=[(None, None)] - ) + temp_extent = pystac.TemporalExtent(intervals=[(None, None)]) item = pystac.Item( id=val, @@ -39,7 +42,7 @@ def create_dummy_item(date=None): properties={}, assets={"ndvi": asset}, href="https://example.com/collections/collection-test3/items/{val}", - collection="collection-test3" + collection="collection-test3", ) col = pystac.Collection( @@ -56,17 +59,16 @@ def create_dummy_item(date=None): METHODS = ["arg", "md", "dict"] -def basic_test( - ext_md, - ext_cls, - item_test: bool = True, - asset_test: bool = True, - collection_test: bool = True, - validate: bool = True +def basic_test( # pylint: disable=too-many-arguments,too-many-positional-arguments + ext_md, + ext_cls, + item_test: bool = True, + asset_test: bool = True, + collection_test: bool = True, + validate: bool = True, ): - print( - f"Extension metadata model: \n{ext_md.__class__.schema_json(indent=2)}" - ) + """Perform some basic tests.""" + print(f"Extension metadata model: \n{ext_md.__class__.schema_json(indent=2)}") def apply(stac_obj, method="arg"): """ @@ -79,10 +81,7 @@ def basic_test( elif method == "md": ext.apply(md=ext_md) elif method == "dict": - d = { - name: getattr(ext_md, name) - for name in ext_md.__fields__ - } + d = {name: getattr(ext_md, name) for name in ext_md.__fields__} print(f"Passing kwargs: {d}") ext.apply(**d) @@ -130,7 +129,7 @@ def basic_test( """ Test extension against collection """ - item, col = create_dummy_item() + _, col = create_dummy_item() print_item(col) apply(col, method) print_item(col) @@ -152,10 +151,11 @@ def basic_test( def is_schema_url_synced(cls): - import requests + """Check that the hosted schema is up-to-date.""" + local_schema = cls.get_schema() url = cls.get_schema_uri() - remote_schema = requests.get(url).json() + remote_schema = requests.get(url, timeout=10).json() print( f"Local schema is :\n" f"{local_schema}\n" @@ -165,15 +165,10 @@ def is_schema_url_synced(cls): ) if local_schema != remote_schema: print("Schema differs:") - import difflib + def _json2str(dic): return json.dumps(dic, indent=2).split("\n") - diff = difflib.unified_diff( - _json2str(local_schema), - _json2str(remote_schema) - ) + diff = difflib.unified_diff(_json2str(local_schema), _json2str(remote_schema)) print("\n".join(diff)) - raise ValueError( - f"Please update the schema located in {url}" - ) + raise ValueError(f"Please update the schema located in {url}") diff --git a/tests/extensions_test.py b/tests/extensions_test.py index 85ec701..1158878 100644 --- a/tests/extensions_test.py +++ b/tests/extensions_test.py @@ -18,21 +18,16 @@ class MyExtensionMetadataModel(BaseModel): name: str = Field(title="Process name", alias=f"{PREFIX}:name") authors: List[str] = Field(title="Authors", alias=f"{PREFIX}:authors") version: str = Field(title="Process version", alias=f"{PREFIX}:version") - opt_field: str | None = Field(title="Some optional field", alias=f"{PREFIX}:opt_field", default=None) + opt_field: str | None = Field( + title="Some optional field", alias=f"{PREFIX}:opt_field", default=None + ) # Create the extension class -MyExtension = create_extension_cls( - model_cls=MyExtensionMetadataModel, - schema_uri=SCHEMA_URI -) +MyExtension = create_extension_cls(model_cls=MyExtensionMetadataModel, schema_uri=SCHEMA_URI) # Metadata fields -ext_md = MyExtensionMetadataModel( - name="test", - authors=["michel", "denis"], - version="alpha" -) +ext_md = MyExtensionMetadataModel(name="test", authors=["michel", "denis"], version="alpha") basic_test(ext_md, MyExtension, validate=False) -- GitLab From 969e44644f5d15d67891e50cd91f4e716fafde79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi?= <remi.cresson@inrae.fr> Date: Tue, 28 Jan 2025 17:17:15 +0100 Subject: [PATCH 2/2] lint, QTY, etc --- pyproject.toml | 4 +--- stac_extension_genmeta/core.py | 11 +++++------ tests/extensions_test.py | 7 +++++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0ad4e6c..5ae2498 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,9 +37,7 @@ version = { attr = "stac_extension_genmeta.__version__" } convention = "google" [tool.mypy] -plugins = [ - "pydantic.mypy" -] +plugins = ["pydantic.mypy"] show_error_codes = true pretty = true exclude = ["doc", "venv", ".venv"] diff --git a/stac_extension_genmeta/core.py b/stac_extension_genmeta/core.py index d9a9333..4bc5be0 100644 --- a/stac_extension_genmeta/core.py +++ b/stac_extension_genmeta/core.py @@ -1,9 +1,8 @@ -""" -Processing extension -""" +# mypy: ignore-errors +"""Processing extension.""" import re -from typing import Any, Generic, TypeVar, Union, cast +from typing import Any, Generic, TypeVar, Union, cast, Type from collections.abc import Iterable import json from pystac.extensions.base import PropertiesExtension, ExtensionManagementMixin @@ -12,7 +11,7 @@ from pydantic import BaseModel from .schema import generate_schema -def create_extension_cls(model_cls: BaseModel, schema_uri: str) -> PropertiesExtension: +def create_extension_cls(model_cls: Type[BaseModel], schema_uri: str): """This method creates a pystac extension from a pydantic model. Args: @@ -58,7 +57,7 @@ def create_extension_cls(model_cls: BaseModel, schema_uri: str) -> PropertiesExt for key, info in model_cls.__fields__.items() } props = {p: v for p, v in props.items() if v is not None} - self.md = model_cls(**props) if props else None + self.md = model_cls.parse_obj(**props) if props else None def __getattr__(self, item): """Forward getattr to `self.md`.""" diff --git a/tests/extensions_test.py b/tests/extensions_test.py index 1158878..d2ae7d9 100644 --- a/tests/extensions_test.py +++ b/tests/extensions_test.py @@ -1,7 +1,9 @@ +# mypy: ignore-errors +"""Tests.""" from stac_extension_genmeta import create_extension_cls from stac_extension_genmeta.testing import basic_test from pydantic import BaseModel, Field, ConfigDict -from typing import List +from typing import List, Type # Extension parameters SCHEMA_URI: str = "https://example.com/image-process/v1.0.0/schema.json" @@ -24,7 +26,8 @@ class MyExtensionMetadataModel(BaseModel): # Create the extension class -MyExtension = create_extension_cls(model_cls=MyExtensionMetadataModel, schema_uri=SCHEMA_URI) +model_cls: Type[BaseModel] = MyExtensionMetadataModel +MyExtension = create_extension_cls(model_cls=model_cls, schema_uri=SCHEMA_URI) # Metadata fields ext_md = MyExtensionMetadataModel(name="test", authors=["michel", "denis"], version="alpha") -- GitLab