From b8b6cfef7bd3b00f3cf99246ca28ffd5ebcdc287 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi?= <remi.cresson@inrae.fr> Date: Thu, 13 Feb 2025 20:48:30 +0100 Subject: [PATCH 01/13] wip --- pyproject.toml | 2 +- stac_extension_genmeta/core.py | 6 +++--- stac_extension_genmeta/testing.py | 12 +++++++----- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9169482..c49f38f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ genmeta_cli = "stac_extension_genmeta.cli:app" packages = ["stac_extension_genmeta"] [project.optional-dependencies] -validation = ["requests", "pystac[validation]"] +test = ["requests", "pystac[validation]", "difflib"] [tool.setuptools.dynamic] version = { attr = "stac_extension_genmeta.__version__" } diff --git a/stac_extension_genmeta/core.py b/stac_extension_genmeta/core.py index c8b1eb2..15ace43 100644 --- a/stac_extension_genmeta/core.py +++ b/stac_extension_genmeta/core.py @@ -12,7 +12,7 @@ from .schema import generate_schema import json -class BaseExtensionModel(BaseException): +class BaseExtensionModel(BaseModel): """Base class for extensions models.""" model_config = ConfigDict(populate_by_name=True) @@ -59,7 +59,7 @@ def create_extension_cls(model_cls: BaseModel, schema_uri: str) -> PropertiesExt # If not possible, self.md is set to `None` props = { key: self._get_property(info.alias, str) - for key, info in model_cls.__fields__.items() + for key, info in model_cls.model_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 @@ -81,7 +81,7 @@ def create_extension_cls(model_cls: BaseModel, schema_uri: str) -> PropertiesExt # Set properties md = md or model_cls(**kwargs) for key, value in md.model_dump(exclude_unset=True).items(): - alias = model_cls.__fields__[key].alias or key + alias = model_cls.model_fields[key].alias or key self._set_property(alias, value, pop_if_none=False) @classmethod diff --git a/stac_extension_genmeta/testing.py b/stac_extension_genmeta/testing.py index 5f51e2d..f134f7c 100644 --- a/stac_extension_genmeta/testing.py +++ b/stac_extension_genmeta/testing.py @@ -2,6 +2,8 @@ import pystac from datetime import datetime import random import json +import requests +import difflib def create_dummy_item(date=None): @@ -65,9 +67,11 @@ def basic_test( validate: bool = True ): print( - f"Extension metadata model: \n{ext_md.__class__.schema_json(indent=2)}" + f"Extension metadata model: \n{ext_md.__class__.model_json_schema()}" ) + ext_cls.print_schema() + def apply(stac_obj, method="arg"): """ Apply the extension to the item @@ -81,7 +85,7 @@ def basic_test( elif method == "dict": d = { name: getattr(ext_md, name) - for name in ext_md.__fields__ + for name in ext_md.model_fields } print(f"Passing kwargs: {d}") ext.apply(**d) @@ -97,7 +101,7 @@ def basic_test( Compare the metadata carried by the stac object with the expected metadata. """ read_ext = ext_cls(stac_obj) - for field in ext_md.__class__.__fields__: + for field in ext_md.__class__.model_fields: ref = getattr(ext_md, field) got = getattr(read_ext, field) assert got == ref, f"'{field}': values differ: {got} (expected {ref})" @@ -152,7 +156,6 @@ def basic_test( def is_schema_url_synced(cls): - import requests local_schema = cls.get_schema() url = cls.get_schema_uri() remote_schema = requests.get(url).json() @@ -165,7 +168,6 @@ 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") -- GitLab From 5c4b39091f8740388670eb812ecdcc704e0f8c06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi?= <remi.cresson@inrae.fr> Date: Thu, 13 Feb 2025 21:01:18 +0100 Subject: [PATCH 02/13] wip --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4bb2302..4a210d4 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 .[test] - python3 tests/extensions_test.py -- GitLab From 34a3ad94cfc689088467d70e65724625fec0e12b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi?= <remi.cresson@inrae.fr> Date: Thu, 13 Feb 2025 21:06:08 +0100 Subject: [PATCH 03/13] wip --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c49f38f..105cdbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ genmeta_cli = "stac_extension_genmeta.cli:app" packages = ["stac_extension_genmeta"] [project.optional-dependencies] -test = ["requests", "pystac[validation]", "difflib"] +test = ["requests", "pystac[validation]"] [tool.setuptools.dynamic] version = { attr = "stac_extension_genmeta.__version__" } -- GitLab From 9a9de7816d0f4b2adf53ab95e0eecc08ad98b1f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi?= <remi.cresson@inrae.fr> Date: Thu, 13 Feb 2025 21:06:57 +0100 Subject: [PATCH 04/13] wip --- stac_extension_genmeta/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stac_extension_genmeta/__init__.py b/stac_extension_genmeta/__init__.py index ff56c03..55989ae 100644 --- a/stac_extension_genmeta/__init__.py +++ b/stac_extension_genmeta/__init__.py @@ -2,4 +2,4 @@ from .core import create_extension_cls, BaseExtensionModel # noqa -__version__ = "0.1.3" +__version__ = "0.1.3-dev1" -- GitLab From 4c176b89a6a2db947c2ccae388814c6603177baf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi?= <remi.cresson@inrae.fr> Date: Thu, 13 Feb 2025 21:46:50 +0100 Subject: [PATCH 05/13] wip --- stac_extension_genmeta/__init__.py | 2 +- stac_extension_genmeta/testing.py | 61 ++++++++++++++---------------- 2 files changed, 29 insertions(+), 34 deletions(-) diff --git a/stac_extension_genmeta/__init__.py b/stac_extension_genmeta/__init__.py index 55989ae..9e7c95b 100644 --- a/stac_extension_genmeta/__init__.py +++ b/stac_extension_genmeta/__init__.py @@ -2,4 +2,4 @@ from .core import create_extension_cls, BaseExtensionModel # noqa -__version__ = "0.1.3-dev1" +__version__ = "0.1.3-dev2" diff --git a/stac_extension_genmeta/testing.py b/stac_extension_genmeta/testing.py index f134f7c..29623d8 100644 --- a/stac_extension_genmeta/testing.py +++ b/stac_extension_genmeta/testing.py @@ -1,3 +1,5 @@ +"""Testing module.""" +import os import pystac from datetime import datetime import random @@ -17,21 +19,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, @@ -41,7 +41,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( @@ -59,16 +59,14 @@ 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 + 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__.model_json_schema()}" - ) + print(f"Extension metadata model: \n{ext_md.__class__.model_json_schema()}") ext_cls.print_schema() @@ -83,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.model_fields - } + d = {name: getattr(ext_md, name) for name in ext_md.model_fields} print(f"Passing kwargs: {d}") ext.apply(**d) @@ -155,9 +150,13 @@ def basic_test( test_collection(method) +CI_COMMIT_REF_NAME = os.environ.get("CI_COMMIT_REF_NAME") + + def is_schema_url_synced(cls): local_schema = cls.get_schema() url = cls.get_schema_uri() + url = url.replace("/-/raw/main/", f"/-/raw/{CI_COMMIT_REF_NAME}/") if CI_COMMIT_REF_NAME else url remote_schema = requests.get(url).json() print( f"Local schema is :\n" @@ -168,14 +167,10 @@ def is_schema_url_synced(cls): ) if local_schema != remote_schema: print("Schema differs:") + 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}") -- GitLab From 3aaf8fde682937d4bfd328f3fd656db37dacb533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi?= <remi.cresson@inrae.fr> Date: Thu, 13 Feb 2025 22:34:42 +0100 Subject: [PATCH 06/13] wip --- .gitlab-ci.yml | 21 +-------------- .gitlab/ci/base.yml | 23 ++++++++++++++++ LICENSE | 2 +- pyproject.toml | 7 ++--- stac_extension_genmeta/__init__.py | 2 +- stac_extension_genmeta/testing.py | 5 +++- tests/extensions_test.py | 42 +++++++++++++----------------- 7 files changed, 50 insertions(+), 52 deletions(-) create mode 100644 .gitlab/ci/base.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4a210d4..de9b93d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,21 +1,2 @@ include: - - project: "cdos-pub/pycode-quality" - ref: "main" - file: - - ".gitlab/ci/static-analysis.yml" - - ".gitlab/ci/pip.yml" - -stages: - - Static Analysis - - Test - - Pip - -Tests: - image: "registry.forgemia.inra.fr/cdos-pub/pycode-quality/python-venv:3.10" - stage: Test - except: [main, tags] - script: - - pip install pip --upgrade - - pip install .[test] - - python3 tests/extensions_test.py - + - ".gitlab/ci/base.yml" diff --git a/.gitlab/ci/base.yml b/.gitlab/ci/base.yml new file mode 100644 index 0000000..bff0450 --- /dev/null +++ b/.gitlab/ci/base.yml @@ -0,0 +1,23 @@ +include: + - project: "cdos-pub/pycode-quality" + ref: "main" + file: + - ".gitlab/ci/static-analysis.yml" + - ".gitlab/ci/pip.yml" + +variables: + PIP_EXTRA_INDEX_URL: https://forgemia.inra.fr/api/v4/projects/10919/packages/pypi/simple + PACKAGE_INSTALL_EXTRAS: "[test]" + +stages: + - Static Analysis + - Test + - Pip + +Tests: + extends: .static_analysis_with_pip_install + stage: Test + allow_failure: false + script: + - pytest tests/ + diff --git a/LICENSE b/LICENSE index a6b5cd4..6c258c1 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2024 INRAE + Copyright 2024-2025 INRAE Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/pyproject.toml b/pyproject.toml index 105cdbd..fa832e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ description = "Helper to build custom STAC extensions based on pydantic models" authors = [ { name = "Rémi Cresson", email = "remi.cresson@inrae.fr" }, ] -requires-python = ">=3.7" +requires-python = ">=3.8" dependencies = [ "pydantic >= 2.0.0", "pystac", @@ -23,14 +23,11 @@ classifiers = [ "Operating System :: OS Independent", ] -[project.scripts] -genmeta_cli = "stac_extension_genmeta.cli:app" - [tool.setuptools] packages = ["stac_extension_genmeta"] [project.optional-dependencies] -test = ["requests", "pystac[validation]"] +test = ["requests", "pystac[validation]", "pytest"] [tool.setuptools.dynamic] version = { attr = "stac_extension_genmeta.__version__" } diff --git a/stac_extension_genmeta/__init__.py b/stac_extension_genmeta/__init__.py index 9e7c95b..9c021c3 100644 --- a/stac_extension_genmeta/__init__.py +++ b/stac_extension_genmeta/__init__.py @@ -2,4 +2,4 @@ from .core import create_extension_cls, BaseExtensionModel # noqa -__version__ = "0.1.3-dev2" +__version__ = "0.1.3-dev3" diff --git a/stac_extension_genmeta/testing.py b/stac_extension_genmeta/testing.py index 29623d8..47c0d32 100644 --- a/stac_extension_genmeta/testing.py +++ b/stac_extension_genmeta/testing.py @@ -1,4 +1,5 @@ """Testing module.""" + import os import pystac from datetime import datetime @@ -156,7 +157,9 @@ CI_COMMIT_REF_NAME = os.environ.get("CI_COMMIT_REF_NAME") def is_schema_url_synced(cls): local_schema = cls.get_schema() url = cls.get_schema_uri() - url = url.replace("/-/raw/main/", f"/-/raw/{CI_COMMIT_REF_NAME}/") if CI_COMMIT_REF_NAME else url + url = ( + url.replace("/-/raw/main/", f"/-/raw/{CI_COMMIT_REF_NAME}/") if CI_COMMIT_REF_NAME else url + ) remote_schema = requests.get(url).json() print( f"Local schema is :\n" diff --git a/tests/extensions_test.py b/tests/extensions_test.py index 85ec701..0172862 100644 --- a/tests/extensions_test.py +++ b/tests/extensions_test.py @@ -1,38 +1,32 @@ -from stac_extension_genmeta import create_extension_cls +from stac_extension_genmeta import create_extension_cls, BaseExtensionModel from stac_extension_genmeta.testing import basic_test -from pydantic import BaseModel, Field, ConfigDict -from typing import List +from pydantic import Field +from typing import List, Final # Extension parameters -SCHEMA_URI: str = "https://example.com/image-process/v1.0.0/schema.json" -PREFIX: str = "some_prefix" +SCHEMA_URI: Final = "https://example.com/image-process/v1.0.0/schema.json" +PREFIX: Final = "some_prefix:" +NAME: Final = PREFIX + "name" +AUTHORS: Final = PREFIX + "authors" +VERSION: Final = PREFIX + "version" -# Extension metadata model -class MyExtensionMetadataModel(BaseModel): - # Required so that one model can be instantiated with the attribute name - # rather than the alias - model_config = ConfigDict(populate_by_name=True) +class MyExtensionMetadataModel(BaseExtensionModel): + """Extension metadata model.""" - # Metadata fields - 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) + name: str = Field(title="Process name", alias=NAME) + authors: List[str] = Field(title="Authors", alias=AUTHORS) + version: str = Field(title="Process version", alias=VERSION) + 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 d41540eedeeed5b4d16ce8930c270e638e838fe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi?= <remi.cresson@inrae.fr> Date: Thu, 13 Feb 2025 22:40:52 +0100 Subject: [PATCH 07/13] wip --- stac_extension_genmeta/core.py | 22 ++++++++++++++++------ stac_extension_genmeta/schema.py | 1 + tests/extensions_test.py | 15 ++++++++------- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/stac_extension_genmeta/core.py b/stac_extension_genmeta/core.py index 15ace43..ce5f35a 100644 --- a/stac_extension_genmeta/core.py +++ b/stac_extension_genmeta/core.py @@ -1,6 +1,4 @@ -""" -Processing extension -""" +"""Generic metadata creation.""" from typing import Any, Generic, TypeVar, Union, cast from pystac.extensions.base import PropertiesExtension, ExtensionManagementMixin @@ -19,8 +17,7 @@ class BaseExtensionModel(BaseModel): def create_extension_cls(model_cls: BaseModel, schema_uri: str) -> PropertiesExtension: - """ - This method creates a pystac extension from a pydantic model. + """This method creates a pystac extension from a pydantic model. Args: model_cls: pydantic model class @@ -45,7 +42,9 @@ def create_extension_cls(model_cls: BaseModel, schema_uri: str) -> PropertiesExt PropertiesExtension, 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)): @@ -65,10 +64,11 @@ def create_extension_cls(model_cls: BaseModel, schema_uri: str) -> PropertiesExt 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 the metadata.""" if md is None and not kwargs: raise ValueError("At least `md` or kwargs is required") @@ -86,10 +86,12 @@ def create_extension_cls(model_cls: BaseModel, schema_uri: str) -> PropertiesExt @classmethod def get_schema_uri(cls) -> str: + """Get schema URI.""" return schema_uri @classmethod def get_schema(cls) -> dict: + """Get schema as dict.""" return generate_schema( model_cls=model_cls, title=f"STAC extension from {model_cls.__name__} model", @@ -99,6 +101,7 @@ def create_extension_cls(model_cls: BaseModel, schema_uri: str) -> PropertiesExt @classmethod def print_schema(cls): + """Print 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,11 +110,13 @@ def create_extension_cls(model_cls: BaseModel, schema_uri: str) -> PropertiesExt @classmethod def export_schema(cls, json_file): + """Export schema.""" with open(json_file, "w") as f: json.dump(cls.get_schema(), f, indent=2) @classmethod def ext(cls, obj: T, add_if_missing: bool = False) -> model_cls.__name__: + """Create the extension.""" if isinstance(obj, pystac.Item): cls.ensure_has_extension(obj, add_if_missing) return cast(CustomExtension[T], ItemCustomExtension(obj)) @@ -126,23 +131,28 @@ def create_extension_cls(model_cls: BaseModel, schema_uri: str) -> PropertiesExt ) class ItemCustomExtension(CustomExtension[pystac.Item]): + """Item custom extension.""" pass 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): + """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 curstom extension.""" properties: dict[str, Any] def __init__(self, collection: pystac.Collection): + """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..53c6c57 100644 --- a/stac_extension_genmeta/schema.py +++ b/stac_extension_genmeta/schema.py @@ -7,6 +7,7 @@ def generate_schema( description: str, schema_uri: str ) -> dict: + """Generate the schema.""" properties = model_cls.model_json_schema() # prune "required" properties.pop("required", None) diff --git a/tests/extensions_test.py b/tests/extensions_test.py index 0172862..71480f0 100644 --- a/tests/extensions_test.py +++ b/tests/extensions_test.py @@ -1,3 +1,5 @@ +"""Tests example.""" + from stac_extension_genmeta import create_extension_cls, BaseExtensionModel from stac_extension_genmeta.testing import basic_test from pydantic import Field @@ -22,12 +24,11 @@ class MyExtensionMetadataModel(BaseExtensionModel): ) -# Create the extension class -MyExtension = create_extension_cls(model_cls=MyExtensionMetadataModel, schema_uri=SCHEMA_URI) - -# Metadata fields -ext_md = MyExtensionMetadataModel(name="test", authors=["michel", "denis"], version="alpha") +def test_example(): + # Create the extension class + MyExtension = create_extension_cls(model_cls=MyExtensionMetadataModel, schema_uri=SCHEMA_URI) -basic_test(ext_md, MyExtension, validate=False) + # Metadata fields + ext_md = MyExtensionMetadataModel(name="test", authors=["michel", "denis"], version="alpha") -MyExtension.print_schema() + basic_test(ext_md, MyExtension, validate=False) -- GitLab From 49532ac556c3800316dd6b1fee100ce96a7eb79f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi?= <remi.cresson@inrae.fr> Date: Thu, 13 Feb 2025 22:44:27 +0100 Subject: [PATCH 08/13] wip --- stac_extension_genmeta/testing.py | 27 +++++++++------------------ tests/extensions_test.py | 7 ++----- 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/stac_extension_genmeta/testing.py b/stac_extension_genmeta/testing.py index 47c0d32..40f8619 100644 --- a/stac_extension_genmeta/testing.py +++ b/stac_extension_genmeta/testing.py @@ -10,6 +10,7 @@ import difflib def create_dummy_item(date=None): + """Create dummy item.""" if not date: date = datetime.now().replace(year=1999) @@ -67,14 +68,13 @@ def basic_test( collection_test: bool = True, validate: bool = True, ): + """Perform the basic testing of the extension class.""" print(f"Extension metadata model: \n{ext_md.__class__.model_json_schema()}") ext_cls.print_schema() def apply(stac_obj, method="arg"): - """ - Apply the extension to the item - """ + """Apply the extension to the item.""" print(f"Check extension applied to {stac_obj.__class__.__name__}") ext = ext_cls.ext(stac_obj, add_if_missing=True) if method == "arg": @@ -87,15 +87,11 @@ def basic_test( ext.apply(**d) def print_item(item): - """ - Print item as JSON - """ + """Print item as JSON.""" print(json.dumps(item.to_dict(), indent=2)) def comp(stac_obj): - """ - Compare the metadata carried by the stac object with the expected metadata. - """ + """Compare the metadata carried by the stac object with the expected metadata.""" read_ext = ext_cls(stac_obj) for field in ext_md.__class__.model_fields: ref = getattr(ext_md, field) @@ -103,9 +99,7 @@ def basic_test( assert got == ref, f"'{field}': values differ: {got} (expected {ref})" def test_item(method): - """ - Test extension against item - """ + """Test extension against item.""" item, _ = create_dummy_item() apply(item, method) print_item(item) @@ -115,9 +109,7 @@ def basic_test( comp(item) def test_asset(method): - """ - Test extension against asset - """ + """Test extension against asset.""" item, _ = create_dummy_item() apply(item.assets["ndvi"], method) print_item(item) @@ -127,9 +119,7 @@ def basic_test( comp(item.assets["ndvi"]) def test_collection(method): - """ - Test extension against collection - """ + """Test extension against collection.""" item, col = create_dummy_item() print_item(col) apply(col, method) @@ -155,6 +145,7 @@ CI_COMMIT_REF_NAME = os.environ.get("CI_COMMIT_REF_NAME") def is_schema_url_synced(cls): + """Check if the schema is in sync with the repository.""" local_schema = cls.get_schema() url = cls.get_schema_uri() url = ( diff --git a/tests/extensions_test.py b/tests/extensions_test.py index 71480f0..8e19920 100644 --- a/tests/extensions_test.py +++ b/tests/extensions_test.py @@ -14,7 +14,7 @@ VERSION: Final = PREFIX + "version" class MyExtensionMetadataModel(BaseExtensionModel): - """Extension metadata model.""" + """Extension metadata model example.""" name: str = Field(title="Process name", alias=NAME) authors: List[str] = Field(title="Authors", alias=AUTHORS) @@ -25,10 +25,7 @@ class MyExtensionMetadataModel(BaseExtensionModel): def test_example(): - # Create the extension class + """Test example function.""" MyExtension = create_extension_cls(model_cls=MyExtensionMetadataModel, schema_uri=SCHEMA_URI) - - # Metadata fields ext_md = MyExtensionMetadataModel(name="test", authors=["michel", "denis"], version="alpha") - basic_test(ext_md, MyExtension, validate=False) -- GitLab From 1f1feab1ea7c5960c267a692695af7dbe7d4c82e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi?= <remi.cresson@inrae.fr> Date: Thu, 13 Feb 2025 22:49:16 +0100 Subject: [PATCH 09/13] wip --- stac_extension_genmeta/core.py | 6 +- stac_extension_genmeta/schema.py | 121 +++++++++--------------------- stac_extension_genmeta/testing.py | 4 +- tests/extensions_test.py | 8 +- 4 files changed, 49 insertions(+), 90 deletions(-) diff --git a/stac_extension_genmeta/core.py b/stac_extension_genmeta/core.py index ce5f35a..a83e350 100644 --- a/stac_extension_genmeta/core.py +++ b/stac_extension_genmeta/core.py @@ -43,6 +43,7 @@ def create_extension_cls(model_cls: BaseModel, schema_uri: str) -> PropertiesExt ExtensionManagementMixin[Union[pystac.Item, pystac.Collection]], ): """Custom extension class.""" + def __init__(self, obj: T): """Initializer.""" if isinstance(obj, pystac.Item): @@ -64,7 +65,7 @@ def create_extension_cls(model_cls: BaseModel, schema_uri: str) -> PropertiesExt 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: @@ -132,10 +133,12 @@ def create_extension_cls(model_cls: BaseModel, schema_uri: str) -> PropertiesExt class ItemCustomExtension(CustomExtension[pystac.Item]): """Item custom extension.""" + pass class AssetCustomExtension(CustomExtension[pystac.Asset]): """Asset custom extension.""" + asset_href: str properties: dict[str, Any] additional_read_properties: Iterable[dict[str, Any]] | None = None @@ -149,6 +152,7 @@ def create_extension_cls(model_cls: BaseModel, schema_uri: str) -> PropertiesExt class CollectionCustomExtension(CustomExtension[pystac.Collection]): """Collection curstom extension.""" + properties: dict[str, Any] def __init__(self, collection: pystac.Collection): diff --git a/stac_extension_genmeta/schema.py b/stac_extension_genmeta/schema.py index 53c6c57..35d33f6 100644 --- a/stac_extension_genmeta/schema.py +++ b/stac_extension_genmeta/schema.py @@ -1,11 +1,10 @@ +"""Generate the json schema.""" + from pydantic import BaseModel def generate_schema( - model_cls: BaseModel, - title: str, - description: str, - schema_uri: str + model_cls: BaseModel, title: str, description: str, schema_uri: str ) -> dict: """Generate the schema.""" properties = model_cls.model_json_schema() @@ -22,116 +21,66 @@ 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" - } - } + "type": {"const": "Collection"}, + "assets": {"$ref": "#/definitions/assets"}, + "item_assets": {"$ref": "#/definitions/assets"}, + "links": {"$ref": "#/definitions/links"}, + }, }, - { - "$ref": "#/definitions/fields" - }, - { - "$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" - } - } + "type": {"const": "Catalog"}, + "links": {"$ref": "#/definitions/links"}, + }, }, - { - "$ref": "#/definitions/fields" - }, - { - "$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 - } + "contains": {"const": schema_uri}, } - } - }, - "links": { - "type": "array", - "items": { - "$ref": "#/definitions/fields" - } + }, }, + "links": {"type": "array", "items": {"$ref": "#/definitions/fields"}}, "assets": { "type": "object", - "additionalProperties": { - "$ref": "#/definitions/fields" - } + "additionalProperties": {"$ref": "#/definitions/fields"}, }, - "fields": properties - } + "fields": properties, + }, } diff --git a/stac_extension_genmeta/testing.py b/stac_extension_genmeta/testing.py index 40f8619..d6986e9 100644 --- a/stac_extension_genmeta/testing.py +++ b/stac_extension_genmeta/testing.py @@ -149,7 +149,9 @@ def is_schema_url_synced(cls): local_schema = cls.get_schema() url = cls.get_schema_uri() url = ( - url.replace("/-/raw/main/", f"/-/raw/{CI_COMMIT_REF_NAME}/") if CI_COMMIT_REF_NAME else url + url.replace("/-/raw/main/", f"/-/raw/{CI_COMMIT_REF_NAME}/") + if CI_COMMIT_REF_NAME + else url ) remote_schema = requests.get(url).json() print( diff --git a/tests/extensions_test.py b/tests/extensions_test.py index 8e19920..69a332c 100644 --- a/tests/extensions_test.py +++ b/tests/extensions_test.py @@ -26,6 +26,10 @@ class MyExtensionMetadataModel(BaseExtensionModel): def test_example(): """Test example function.""" - MyExtension = create_extension_cls(model_cls=MyExtensionMetadataModel, schema_uri=SCHEMA_URI) - ext_md = MyExtensionMetadataModel(name="test", authors=["michel", "denis"], version="alpha") + MyExtension = create_extension_cls( + model_cls=MyExtensionMetadataModel, schema_uri=SCHEMA_URI + ) + ext_md = MyExtensionMetadataModel( + name="test", authors=["michel", "denis"], version="alpha" + ) basic_test(ext_md, MyExtension, validate=False) -- GitLab From 6feba1cd544164c065774252fc27b6d9594b34b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi?= <remi.cresson@inrae.fr> Date: Thu, 13 Feb 2025 22:53:15 +0100 Subject: [PATCH 10/13] wip --- stac_extension_genmeta/core.py | 17 ++++------------- stac_extension_genmeta/testing.py | 6 +++--- tests/extensions_test.py | 4 ++-- 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/stac_extension_genmeta/core.py b/stac_extension_genmeta/core.py index a83e350..c46fff2 100644 --- a/stac_extension_genmeta/core.py +++ b/stac_extension_genmeta/core.py @@ -1,13 +1,13 @@ """Generic metadata creation.""" +from collections.abc import Iterable +import json +import re from typing import Any, Generic, TypeVar, Union, cast from pystac.extensions.base import PropertiesExtension, ExtensionManagementMixin import pystac from pydantic import BaseModel, ConfigDict -import re -from collections.abc import Iterable from .schema import generate_schema -import json class BaseExtensionModel(BaseModel): @@ -17,16 +17,7 @@ class BaseExtensionModel(BaseModel): 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 - schema_uri: schema URI - - Returns: - pystac extension class - - """ + """This method creates a pystac extension from a pydantic model.""" # check URI if not re.findall(r"(?:(\/v\d\.(?:\d+\.)*\d+\/+))", schema_uri): diff --git a/stac_extension_genmeta/testing.py b/stac_extension_genmeta/testing.py index d6986e9..23c5bc8 100644 --- a/stac_extension_genmeta/testing.py +++ b/stac_extension_genmeta/testing.py @@ -1,12 +1,12 @@ """Testing module.""" import os -import pystac -from datetime import datetime import random import json -import requests import difflib +from datetime import datetime +import requests +import pystac def create_dummy_item(date=None): diff --git a/tests/extensions_test.py b/tests/extensions_test.py index 69a332c..6fcf78c 100644 --- a/tests/extensions_test.py +++ b/tests/extensions_test.py @@ -1,9 +1,9 @@ """Tests example.""" -from stac_extension_genmeta import create_extension_cls, BaseExtensionModel -from stac_extension_genmeta.testing import basic_test from pydantic import Field from typing import List, Final +from stac_extension_genmeta import create_extension_cls, BaseExtensionModel +from stac_extension_genmeta.testing import basic_test # Extension parameters SCHEMA_URI: Final = "https://example.com/image-process/v1.0.0/schema.json" -- GitLab From 9d4812f54d4575fe3e650f3b752321f2fd294590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi?= <remi.cresson@inrae.fr> Date: Thu, 13 Feb 2025 23:07:54 +0100 Subject: [PATCH 11/13] wip --- stac_extension_genmeta/core.py | 8 +++----- stac_extension_genmeta/testing.py | 10 ++++------ tests/extensions_test.py | 5 +++-- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/stac_extension_genmeta/core.py b/stac_extension_genmeta/core.py index c46fff2..10dcf76 100644 --- a/stac_extension_genmeta/core.py +++ b/stac_extension_genmeta/core.py @@ -103,7 +103,7 @@ def create_extension_cls(model_cls: BaseModel, schema_uri: str) -> PropertiesExt @classmethod def export_schema(cls, json_file): """Export schema.""" - with open(json_file, "w") as f: + with open(json_file, "w", encoding="utf-8") as f: json.dump(cls.get_schema(), f, indent=2) @classmethod @@ -112,10 +112,10 @@ def create_extension_cls(model_cls: BaseModel, schema_uri: str) -> PropertiesExt 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( @@ -125,8 +125,6 @@ def create_extension_cls(model_cls: BaseModel, schema_uri: str) -> PropertiesExt class ItemCustomExtension(CustomExtension[pystac.Item]): """Item custom extension.""" - pass - class AssetCustomExtension(CustomExtension[pystac.Asset]): """Asset custom extension.""" diff --git a/stac_extension_genmeta/testing.py b/stac_extension_genmeta/testing.py index 23c5bc8..319ae16 100644 --- a/stac_extension_genmeta/testing.py +++ b/stac_extension_genmeta/testing.py @@ -63,7 +63,6 @@ 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, @@ -120,7 +119,7 @@ def basic_test( def test_collection(method): """Test extension against collection.""" - item, col = create_dummy_item() + _, col = create_dummy_item() print_item(col) apply(col, method) print_item(col) @@ -130,9 +129,8 @@ def basic_test( comp(col) for method in METHODS: - if item_test: - print(f"Test item with {method} args passing strategy") - test_item(method) + print(f"Test item with {method} args passing strategy") + test_item(method) if asset_test: print(f"Test asset with {method} args passing strategy") test_asset(method) @@ -153,7 +151,7 @@ def is_schema_url_synced(cls): if CI_COMMIT_REF_NAME else url ) - remote_schema = requests.get(url).json() + remote_schema = requests.get(url, timeout=10).json() print( f"Local schema is :\n" f"{local_schema}\n" diff --git a/tests/extensions_test.py b/tests/extensions_test.py index 6fcf78c..bbae3cb 100644 --- a/tests/extensions_test.py +++ b/tests/extensions_test.py @@ -1,7 +1,7 @@ """Tests example.""" -from pydantic import Field from typing import List, Final +from pydantic import Field from stac_extension_genmeta import create_extension_cls, BaseExtensionModel from stac_extension_genmeta.testing import basic_test @@ -11,6 +11,7 @@ PREFIX: Final = "some_prefix:" NAME: Final = PREFIX + "name" AUTHORS: Final = PREFIX + "authors" VERSION: Final = PREFIX + "version" +OPT_FIELD: Final = PREFIX + "opt_field" class MyExtensionMetadataModel(BaseExtensionModel): @@ -20,7 +21,7 @@ class MyExtensionMetadataModel(BaseExtensionModel): authors: List[str] = Field(title="Authors", alias=AUTHORS) version: str = Field(title="Process version", alias=VERSION) opt_field: str | None = Field( - title="Some optional field", alias=f"{PREFIX}:opt_field", default=None + title="Some optional field", alias=OPT_FIELD, default=None ) -- GitLab From 981506e1336e51b9361fdda8ccc8d9b23c1e2388 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi?= <remi.cresson@inrae.fr> Date: Thu, 13 Feb 2025 23:21:58 +0100 Subject: [PATCH 12/13] wip --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 71f0b57..017b99c 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,13 @@ This module is a helper to build STAC extensions carrying metadata defined with pydantic models. +## Installation + +``` +PIP_EXTRA_INDEX_URL=https://forgemia.inra.fr/api/v4/projects/10919/packages/pypi/simple +pip install stac-extension-genmeta +``` + ## Example Simple example in 4 steps. -- GitLab From bb1dd68d7e17687224f018141aad5a3e2dab2af5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi?= <remi.cresson@inrae.fr> Date: Thu, 13 Feb 2025 23:24:40 +0100 Subject: [PATCH 13/13] wip --- stac_extension_genmeta/core.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/stac_extension_genmeta/core.py b/stac_extension_genmeta/core.py index 10dcf76..2f53023 100644 --- a/stac_extension_genmeta/core.py +++ b/stac_extension_genmeta/core.py @@ -18,8 +18,6 @@ class BaseExtensionModel(BaseModel): def create_extension_cls(model_cls: BaseModel, schema_uri: str) -> PropertiesExtension: """This method creates a pystac extension from a pydantic model.""" - - # check URI if not re.findall(r"(?:(\/v\d\.(?:\d+\.)*\d+\/+))", schema_uri): raise ValueError( "The schema_uri must contain the version in the form 'vX.Y.Z'" -- GitLab