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