# HG changeset patch # User Vincent Hatakeyama <vincent.hatakeyama@xcg-consulting.fr> # Date 1734017944 -3600 # Thu Dec 12 16:39:04 2024 +0100 # Branch 18.0 # Node ID 641c8be448fb1faca0b5576092cc0565d2954be3 # Parent a3cfdf8a720d4f1d29982f70beda7dadbde6e86b 🔨✨ validator package does not assume a odoo.addons package name, provide full package name instead diff --git a/NEWS.rst b/NEWS.rst --- a/NEWS.rst +++ b/NEWS.rst @@ -1,6 +1,11 @@ Changelog ========= +18.0.3.0.0 +---------- + +Breaking change: validator package does not assume a odoo.addons package name, provide full package name instead. + 18.0.2.2.0 ---------- diff --git a/__manifest__.py b/__manifest__.py --- a/__manifest__.py +++ b/__manifest__.py @@ -21,7 +21,7 @@ "name": "Converter", "license": "AGPL-3", "summary": "Convert odoo records to/from plain data structures.", - "version": "18.0.2.2.0", + "version": "18.0.3.0.0", "category": "Hidden", "author": "XCG Consulting", "website": "https://orbeet.io/", diff --git a/pyproject.toml b/pyproject.toml --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,8 @@ "*.xml", "*.py", "*.svg", - "*.png" + "*.png", + "*.json" ] [tool.hatch.build.targets.wheel.sources] diff --git a/tests/__init__.py b/tests/__init__.py --- a/tests/__init__.py +++ b/tests/__init__.py @@ -6,4 +6,5 @@ test_mail_template, test_relation, test_switch, + test_validate, ) diff --git a/tests/schemas/__init__.py b/tests/schemas/__init__.py new file mode 100644 --- /dev/null +++ b/tests/schemas/__init__.py @@ -0,0 +1,10 @@ +import json +import pkgutil +from typing import Optional, Any + + +def get_schemas() -> list[Any]: + for file_prefix in ("product",): + data: bytes | None = pkgutil.get_data(__name__, f"{file_prefix}.schema.json") + if data: + yield json.loads(data) diff --git a/tests/schemas/product.schema.json b/tests/schemas/product.schema.json new file mode 100644 --- /dev/null +++ b/tests/schemas/product.schema.json @@ -0,0 +1,71 @@ +{ + + "$schema": "https://json-schema.org/draft/2020-12/schema", + + "$id": "https://example.com/product.schema.json", + + "title": "Product", + + "description": "A product from Acme's catalog", + + "type": "object", + + "properties": { + + "productId": { + + "description": "The unique identifier for a product", + + "type": "integer" + + }, + + "productName": { + + "description": "Name of the product", + + "type": "string" + + }, + + "price": { + + "description": "The price of the product", + + "type": "number", + + "exclusiveMinimum": 0 + + }, + + "tags": { + + "description": "Tags for the product", + + "type": "array", + + "items": { + + "type": "string" + + }, + + "minItems": 1, + + "uniqueItems": true + + } + + }, + + "required": [ + + "productId", + + "productName", + + "price" + + ] + +} diff --git a/tests/schemas_dir/product.schema.json b/tests/schemas_dir/product.schema.json new file mode 100644 --- /dev/null +++ b/tests/schemas_dir/product.schema.json @@ -0,0 +1,71 @@ +{ + + "$schema": "https://json-schema.org/draft/2020-12/schema", + + "$id": "https://example.com/product.schema.json", + + "title": "Product", + + "description": "A product from Acme's catalog", + + "type": "object", + + "properties": { + + "productId": { + + "description": "The unique identifier for a product", + + "type": "integer" + + }, + + "productName": { + + "description": "Name of the product", + + "type": "string" + + }, + + "price": { + + "description": "The price of the product", + + "type": "number", + + "exclusiveMinimum": 0 + + }, + + "tags": { + + "description": "Tags for the product", + + "type": "array", + + "items": { + + "type": "string" + + }, + + "minItems": 1, + + "uniqueItems": true + + } + + }, + + "required": [ + + "productId", + + "productName", + + "price" + + ] + +} diff --git a/tests/test_validate.py b/tests/test_validate.py new file mode 100644 --- /dev/null +++ b/tests/test_validate.py @@ -0,0 +1,64 @@ +############################################################################## +# +# Converter Odoo module +# Copyright © 2024 XCG Consulting <https://xcg-consulting.fr> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +############################################################################## +import json + +from odoo import tests # type: ignore[import-untyped] + +from ..validate import Validator + + +class TestValidate(tests.TransactionCase): + def test_validate(self): + validator = Validator( + "odoo.addons.converter.tests.schemas", "https://example.com/{}.schema.json" + ) + validator.initialize() + validator.validate( + "product", + json.loads("""{ + "productId": 1, + "productName": "An ice sculpture", + "price": 12.5, + "tags": [ + "cold", + "ice" + ] +}"""), + ) + + def test_validate_dir(self): + validator = Validator( + "odoo.addons.converter.tests", + "https://example.com/{}.schema.json", + "schemas_dir", + ) + validator.initialize() + validator.validate( + "product", + json.loads("""{ + "productId": 1, + "productName": "An ice sculpture", + "price": 12.5, + "tags": [ + "cold", + "ice" + ] +}"""), + ) diff --git a/validate.py b/validate.py --- a/validate.py +++ b/validate.py @@ -18,12 +18,15 @@ # ############################################################################## import json +import logging import os +from collections.abc import Callable from enum import Enum +from importlib import import_module from typing import Any, LiteralString import fastjsonschema # type: ignore[import-untyped] -import odoo.addons # type: ignore[import-untyped] +_logger = logging.getLogger(__name__) class Validation(str, Enum): @@ -38,40 +41,57 @@ pass +def _add_schema(schemas, schema): + if "$id" in schema: + schemas[schema["$id"]] = schema + else: + _logger.warning("Schema without $id (schema ignored)") + + class Validator: def __init__( self, - repository_module_name: str, - repository: str, + package_name: str, default_url_pattern: str, + directory: str | None = None, ): - self.repository_module_name = repository_module_name - self.repository = repository + """ + :param package_name: Package where the schema can be found + :param default_url_pattern: pattern for url ({} will be replaced by $id) + :param directory: directory to search for json, not used if a get_schema is + provided in the package. + """ + self.package_name = package_name # exemple "https://annonces-legales.fr/xbus/schemas/v1/{}.schema.json" self.default_url_pattern = default_url_pattern - self.validators: dict[LiteralString, Any] = {} + self.validators: dict[LiteralString, Callable] = {} self.initialized = False self.encoding = "UTF-8" + self.directory = directory def initialize(self) -> None: - # TODO Not working if module is installed compressed - repo_module_basepath = os.path.dirname( - getattr(odoo.addons, self.repository_module_name).__file__ - ) - - # Read local schema definitions. - schemas = {} - schema_search_path = os.path.abspath( - os.path.join(repo_module_basepath, self.repository) - ) - for root, _dirs, files in os.walk(schema_search_path): - for fname in files: - fpath = os.path.join(root, fname) - if fpath.endswith((".json",)): - with open(fpath, encoding=self.encoding) as schema_fd: - schema = json.load(schema_fd) - if "$id" in schema: - schemas[schema["$id"]] = schema + if self.initialized: + return + schemas: dict[LiteralString, Any] = {} + module = import_module(self.package_name) + if hasattr(module, "get_schemas"): + for schema in module.get_schemas(): + _add_schema(schemas, schema) + else: + # Fallback on searching schema json files + schema_search_path = os.path.dirname(module.__file__) + schema_search_path = os.path.abspath( + os.path.join(schema_search_path, self.directory) + if self.directory is not None + else schema_search_path + ) + for root, _dirs, files in os.walk(schema_search_path): + for fname in files: + fpath = os.path.join(root, fname) + if fpath.endswith((".json",)): + with open(fpath, encoding=self.encoding) as schema_fd: + schema = json.load(schema_fd) + _add_schema(schemas, schema) # Prepare validators for each schema. We add an HTTPS handler that # points back to our schema definition cache built above. # HG changeset patch # User szeka_wong <szeka.wong@xcg-consulting.fr> # Date 1734509421 -3600 # Wed Dec 18 09:10:21 2024 +0100 # Branch 18.0 # Node ID 5556bfbd25c5a0e5ed87a123c7e806e8812f6773 # Parent 641c8be448fb1faca0b5576092cc0565d2954be3 Ruff and prettier diff --git a/tests/schemas/product.schema.json b/tests/schemas/product.schema.json --- a/tests/schemas/product.schema.json +++ b/tests/schemas/product.schema.json @@ -1,5 +1,4 @@ { - "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://example.com/product.schema.json", @@ -11,61 +10,40 @@ "type": "object", "properties": { - "productId": { - "description": "The unique identifier for a product", "type": "integer" - }, "productName": { - "description": "Name of the product", "type": "string" - }, "price": { - "description": "The price of the product", "type": "number", "exclusiveMinimum": 0 - }, "tags": { - "description": "Tags for the product", "type": "array", "items": { - "type": "string" - }, "minItems": 1, "uniqueItems": true - } - }, - "required": [ - - "productId", - - "productName", - - "price" - - ] - + "required": ["productId", "productName", "price"] } diff --git a/tests/schemas_dir/product.schema.json b/tests/schemas_dir/product.schema.json --- a/tests/schemas_dir/product.schema.json +++ b/tests/schemas_dir/product.schema.json @@ -1,5 +1,4 @@ { - "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://example.com/product.schema.json", @@ -11,61 +10,40 @@ "type": "object", "properties": { - "productId": { - "description": "The unique identifier for a product", "type": "integer" - }, "productName": { - "description": "Name of the product", "type": "string" - }, "price": { - "description": "The price of the product", "type": "number", "exclusiveMinimum": 0 - }, "tags": { - "description": "Tags for the product", "type": "array", "items": { - "type": "string" - }, "minItems": 1, "uniqueItems": true - } - }, - "required": [ - - "productId", - - "productName", - - "price" - - ] - + "required": ["productId", "productName", "price"] } # HG changeset patch # User Vincent Hatakeyama <vincent.hatakeyama@xcg-consulting.fr> # Date 1734512770 -3600 # Wed Dec 18 10:06:10 2024 +0100 # Branch 18.0 # Node ID 6b2589f5fd7065487f1d5fe7c817d0168df43421 # Parent 5556bfbd25c5a0e5ed87a123c7e806e8812f6773 👕 prettier diff --git a/pyproject.toml b/pyproject.toml --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ "Programming Language :: Python :: 3.11", "Framework :: Odoo", "Framework :: Odoo :: 18.0", - "License :: OSI Approved :: GNU Affero General Public License v3" + "License :: OSI Approved :: GNU Affero General Public License v3", ] dependencies = ["odoo==18.0.*", "fastjsonschema"] @@ -37,7 +37,7 @@ "/.hgignore", "/.hgtags", "/prettier.config.cjs", - "/.yamllint.yaml" + "/.yamllint.yaml", ] [tool.hatch.build.targets.wheel] @@ -51,7 +51,7 @@ "*.py", "*.svg", "*.png", - "*.json" + "*.json", ] [tool.hatch.build.targets.wheel.sources] @@ -69,14 +69,14 @@ "B", "C90", "E501", # line too long (default 88) - "I", # isort - "UP", # pyupgrade + "I", # isort + "UP", # pyupgrade ] [tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401", "I001"] # ignore unused and unsorted imports in __init__.py -"__manifest__.py" = ["B018"] # useless expression +"__manifest__.py" = ["B018"] # useless expression [tool.ruff.lint.mccabe] max-complexity = 16 @@ -89,7 +89,7 @@ "odoo", "odoo-addons", "first-party", - "local-folder" + "local-folder", ] [tool.isort.sections] # HG changeset patch # User Vincent Hatakeyama <vincent.hatakeyama@xcg-consulting.fr> # Date 1734513980 -3600 # Wed Dec 18 10:26:20 2024 +0100 # Branch 18.0 # Node ID ca9832fbd70a95e86bad1491cd4ca7724ee126d5 # Parent 6b2589f5fd7065487f1d5fe7c817d0168df43421 👕 mypy diff --git a/tests/schemas/__init__.py b/tests/schemas/__init__.py --- a/tests/schemas/__init__.py +++ b/tests/schemas/__init__.py @@ -1,9 +1,10 @@ import json import pkgutil -from typing import Optional, Any +from typing import Any +from collections.abc import Generator -def get_schemas() -> list[Any]: +def get_schemas() -> Generator[Any]: for file_prefix in ("product",): data: bytes | None = pkgutil.get_data(__name__, f"{file_prefix}.schema.json") if data: # HG changeset patch # User Vincent Hatakeyama <vincent.hatakeyama@xcg-consulting.fr> # Date 1734514546 -3600 # Wed Dec 18 10:35:46 2024 +0100 # Branch 18.0 # Node ID 993af5be16eb866975e092813f6ce50a933a8dd8 # Parent ca9832fbd70a95e86bad1491cd4ca7724ee126d5 👕 mypy: handle __file__ is none diff --git a/validate.py b/validate.py --- a/validate.py +++ b/validate.py @@ -26,6 +26,9 @@ from typing import Any, LiteralString import fastjsonschema # type: ignore[import-untyped] +from odoo.exceptions import UserError # type: ignore[import-untyped] + +import fastjsonschema # type: ignore[import-untyped] _logger = logging.getLogger(__name__) @@ -78,6 +81,9 @@ for schema in module.get_schemas(): _add_schema(schemas, schema) else: + if module.__file__ is None: + # XXX maybe not the best type of error + raise UserError("Module %s has no file", self.package_name) # Fallback on searching schema json files schema_search_path = os.path.dirname(module.__file__) schema_search_path = os.path.abspath( # HG changeset patch # User Vincent Hatakeyama <vincent.hatakeyama@xcg-consulting.fr> # Date 1734514965 -3600 # Wed Dec 18 10:42:45 2024 +0100 # Branch 18.0 # Node ID a3b3b60ffcc1ccc3ea4ce98a7650d2a9508c56ad # Parent 993af5be16eb866975e092813f6ce50a933a8dd8 👕 ruff diff --git a/validate.py b/validate.py --- a/validate.py +++ b/validate.py @@ -28,7 +28,6 @@ import fastjsonschema # type: ignore[import-untyped] from odoo.exceptions import UserError # type: ignore[import-untyped] -import fastjsonschema # type: ignore[import-untyped] _logger = logging.getLogger(__name__)