diff --git a/.project b/.project index dc36649..2eea672 100644 --- a/.project +++ b/.project @@ -1,6 +1,6 @@ - {{project_name}} + dabmodel diff --git a/src/dabmodel/__init__.py b/src/dabmodel/__init__.py index f817232..0c77e9a 100644 --- a/src/dabmodel/__init__.py +++ b/src/dabmodel/__init__.py @@ -11,4 +11,4 @@ Main module __init__ file. """ from .__metadata__ import __version__, __Summuary__, __Name__ -from .test_module import test_function +from .model import DABField, BaseFeature, BaseAppliance, default_values_override diff --git a/src/dabmodel/model.py b/src/dabmodel/model.py new file mode 100644 index 0000000..eed54f2 --- /dev/null +++ b/src/dabmodel/model.py @@ -0,0 +1,141 @@ +from __future__ import annotations + +from abc import ABC, ABCMeta +from uuid import uuid4 +from typing import Annotated, ClassVar, Any, Callable, Self, TypeVar, TypeAlias +from typing_extensions import dataclass_transform +from pydantic import BaseModel, StrictInt, StrictStr, constr, ByteSize, AwareDatetime, UUID4, model_validator +from pydantic.fields import Field, _Unset, PydanticUndefined +from pydantic._internal._model_construction import ModelMetaclass, PydanticModelField +from pydantic._internal._generics import PydanticGenericMetadata +from pydantic._internal._decorators import ensure_classmethod_based_on_signature +from datetime import datetime +import pytz +from copy import deepcopy + + +def DABField(default: Any = PydanticUndefined, *, final: bool | None = _Unset, finalize_only: bool | None = _Unset, **kwargs): + kwargs["frozen"] = True + return Field(default, **kwargs) + + +T_BaseElement = TypeVar("T_BaseElement", bound="BaseElement") +T_BaseElement_ConfigMethod_Arg: TypeAlias = dict[str, Any] +T_BaseElement_ConfigMethod: TypeAlias = "classmethod[T_BaseElement, [T_BaseElement_ConfigMethod_Arg], T_BaseElement_ConfigMethod_Arg]" +T_BaseElement_ConfigMethod_OrderedSet: TypeAlias = dict[T_BaseElement_ConfigMethod, None] + + +class ConfigElement: + default_values_override_methods: T_BaseElement_ConfigMethod_OrderedSet = {} + main_build_method: T_BaseElement_ConfigMethod_OrderedSet = {} + + +class IBaseElement(ABC): + _config_element: ConfigElement = ConfigElement() + + +@dataclass_transform(kw_only_default=True, field_specifiers=(PydanticModelField,)) +class BaseElementMeta(ModelMetaclass, ABCMeta): + def __new__( + mcs, + cls_name: str, + bases: tuple[type[Any], ...], + namespace: dict[str, Any], + __pydantic_generic_metadata__: PydanticGenericMetadata | None = None, + __pydantic_reset_parent_namespace__: bool = True, + _create_model_module: str | None = None, + **kwargs: Any, + ) -> type: + result = super().__new__( + mcs, + cls_name, + bases, + namespace, + __pydantic_generic_metadata__, + __pydantic_reset_parent_namespace__, + _create_model_module, + **kwargs, + ) + # print(result) + # print(type(result)) + assert issubclass(result, IBaseElement) + # copy base class default-configs + if not "_config_element" in result.__dict__: + # print(result.__base__) + # print(type(result.__base__)) + assert result.__base__ is not None + if issubclass(result.__base__, IBaseElement): + result._config_element = deepcopy(result.__base__._config_element) + # searching and storing current class default-configs + for _, method in result.__dict__.items(): + if isinstance(method, classmethod): + if hasattr(method, "default_values_override"): + result._config_element.default_values_override_methods[method] = None + # todo: find a way to 'lock' and add restriction to a field after inheritance + return result + + +class BaseElement( + IBaseElement, + BaseModel, + ABC, + validate_assignment=True, + revalidate_instances="subclass-instances", + validate_default=True, + metaclass=BaseElementMeta, +): + template_id: Annotated[UUID4, DABField(..., repr=True)] + template_short_name: Annotated[ + StrictStr, constr(strip_whitespace=True, to_lower=True, strict=True, max_length=16), DABField(..., repr=True) + ] + template_long_name: Annotated[StrictStr | None, DABField()] + template_description: Annotated[StrictStr | None, DABField()] + + @model_validator(mode="before") + @classmethod + def __default_values_override_hook__(cls, values: T_BaseElement_ConfigMethod_Arg) -> T_BaseElement_ConfigMethod_Arg: + new_values = deepcopy(values) + for method, _ in cls._config_element.default_values_override_methods.items(): + new_values = method.__func__(cls, new_values) + for key, _ in values.items(): + new_values[key] = values[key] + return new_values + + +def default_values_override(func: T_BaseElement_ConfigMethod) -> T_BaseElement_ConfigMethod: + func = ensure_classmethod_based_on_signature(func) + setattr(func, "default_values_override", lambda: True) + return func + + +class BaseFeature(BaseElement, ABC): + compatible_appliance: Annotated[BaseAppliance, DABField()] + + +# FeatureSet = dict[BaseFeature, None] + + +class BaseAppliance(BaseElement, ABC): + cpu_cnt: Annotated[StrictInt, DABField(1, gt=0)] + ram_size: Annotated[ByteSize, DABField(256, gt=128)] + swap_size: Annotated[ByteSize, DABField(200, ge=0)] + + rootfs_size: Annotated[ByteSize, DABField(2048, ge=2048)] + + dabinst_id: Annotated[UUID4, DABField(uuid4(), repr=True)] + dabinst_short_name: Annotated[ + StrictStr, constr(strip_whitespace=True, to_lower=True, strict=True, max_length=16), DABField(..., repr=True) + ] + dabinst_long_name: Annotated[StrictStr | None, DABField("")] + dabinst_description: Annotated[StrictStr | None, DABField("")] + dabinst_creationdate: Annotated[AwareDatetime | None, DABField(datetime.now(tz=pytz.utc))] + + # __features: ClassVar[FeatureSet] = [] + + def _add_feature(self, feature: BaseFeature) -> None: ... + + # self.__features.append(feature) + + +# def __init__(self, *args, **data): +# super().__init__(*args, **data) diff --git a/src/dabmodel/test_module.py b/src/dabmodel/test_module.py deleted file mode 100644 index bc5a5df..0000000 --- a/src/dabmodel/test_module.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# dabmodel (c) by chacha -# -# dabmodel is licensed under a -# Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Unported License. -# -# You should have received a copy of the license along with this -# work. If not, see . - -"""Phasellus tellus lectus, volutpat eu dapibus ut, suscipit vel augue. - -Tips: - Aliquam non leo vel libero sagittis viverra. Quisque lobortis nunc sit amet augue euismod laoreet. -Note: - Maecenas volutpat porttitor pretium. Aliquam suscipit quis nisi non imperdiet. -Note: - Vivamus et efficitur lorem, eget imperdiet tortor. Integer vel interdum sem. -""" - -from __future__ import annotations -from typing import TYPE_CHECKING - -if TYPE_CHECKING: # Only imports the below statements during type checking - pass - -def test_function(testvar: int) -> int: - """ A test function that return testvar+1 and print "Hello world !" - - Proin eget sapien eget ipsum efficitur mollis nec ac nibh. - - Note: - Morbi id lectus maximus, condimentum nunc eget, porta felis. In tristique velit tortor. - - Args: - testvar: any integer - - Returns: - testvar+1 - """ - print("Hello world !") - return testvar+1 diff --git a/test/test_model.py b/test/test_model.py new file mode 100644 index 0000000..ef21c5b --- /dev/null +++ b/test/test_model.py @@ -0,0 +1,86 @@ +# dabmodel (c) by chacha +# +# dabmodel is licensed under a +# Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Unported License. +# +# You should have received a copy of the license along with this +# work. If not, see . + +import unittest +from os import chdir +from pathlib import Path + +from pydantic import StrictInt, model_validator + +print(__name__) +print(__package__) + +from src import dabmodel +from typing import Annotated, Any +from uuid import uuid4 + +testdir_path = Path(__file__).parent.resolve() +chdir(testdir_path.parent.resolve()) + + +class MyAppliance(dabmodel.BaseAppliance): + + app_specifi_integer_arg: Annotated[StrictInt | None, dabmodel.DABField(42)] + + @dabmodel.default_values_override + @classmethod + def __override_config__(cls, values): + print("!!! CONFIG 1") + values["template_id"] = "421d61cb-e364-46d8-9b64-ec439f1faae8" + values["template_short_name"] = "my-app- tem 1" + values["template_long_name"] = "My appliance template 1 !!" + values["template_description"] = """A very nice Appliance 1""" + values["ram_size"] = 1024 + return values + + +class MyAppliance2(MyAppliance): + @dabmodel.default_values_override + @classmethod + def __override_config__(cls, values): + print("!!! CONFIG 2") + values["template_id"] = "421d61cb-e664-46d8-9b64-ec439f1fafff" + values["template_short_name"] = "my-app- tem 2" + values["template_long_name"] = "My appliance template 2 !!" + values["template_description"] = """A very nice Appliance 2""" + return values + + +class MyAppliance3(dabmodel.BaseAppliance): + @dabmodel.default_values_override + @classmethod + def __override_config__(cls, values): + print("!!! CONFIG 3") + values["template_id"] = "421d61cb-e364-46d8-9b64-ec439f1faaaa" + values["template_short_name"] = "my-app- tem 3" + values["template_long_name"] = "My appliance template 3 !!" + values["template_description"] = """A very nice Appliance 3""" + values["ram_size"] = 3076 + return values + + +class TestModel(unittest.TestCase): + def setUp(self) -> None: + chdir(testdir_path.parent.resolve()) + + def test_version(self): + self.assertNotEqual(dabmodel.__version__, "?.?.?") + + def test_model(self): + + app = MyAppliance(dabinst_short_name="my-app-1", app_specifi_integer_arg=123) + # app.template_short_name = "tete !" + print(app.model_dump_json(indent=1)) + + app2 = MyAppliance2(dabinst_short_name="my-app-2", app_specifi_integer_arg=654) + # app.template_short_name = "tete !" + print(app2.model_dump_json(indent=1)) + + app3 = MyAppliance3(dabinst_short_name="my-app-3", app_specifi_integer_arg=654, template_description="FORCED") + # app.template_short_name = "tete !" + print(app3.model_dump_json(indent=1)) diff --git a/test/test_test_module.py b/test/test_test_module.py deleted file mode 100644 index 714d0f4..0000000 --- a/test/test_test_module.py +++ /dev/null @@ -1,35 +0,0 @@ -# dabmodel (c) by chacha -# -# dabmodel is licensed under a -# Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Unported License. -# -# You should have received a copy of the license along with this -# work. If not, see . - -import unittest -from os import chdir -from io import StringIO -from contextlib import redirect_stdout,redirect_stderr -from pathlib import Path - -print(__name__) -print(__package__) - -from src import dabmodel - -testdir_path = Path(__file__).parent.resolve() -chdir(testdir_path.parent.resolve()) - -class Testtest_module(unittest.TestCase): - def setUp(self) -> None: - chdir(testdir_path.parent.resolve()) - - def test_version(self): - self.assertNotEqual(dabmodel.__version__,"?.?.?") - - def test_test_module(self): - - with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: - self.assertEqual(dabmodel.test_function(41),42) - self.assertEqual(len(capted_stderr.getvalue()),0) - self.assertEqual(capted_stdout.getvalue().strip(),"Hello world !") \ No newline at end of file