kmodels 0.1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kmodels/__init__.py +0 -0
- kmodels/_old/__init__.py +0 -0
- kmodels/_old/_old_models.py +182 -0
- kmodels/error.py +3 -0
- kmodels/models/__init__.py +5 -0
- kmodels/models/coremodel/__init__.py +6 -0
- kmodels/models/coremodel/coremodel.py +276 -0
- kmodels/models/coremodel/leave_unset.py +23 -0
- kmodels/test/__init__.py +0 -0
- kmodels/test/test.py +136 -0
- kmodels/types.py +10 -0
- kmodels/utils.py +77 -0
- kmodels-0.1.0.0.dist-info/METADATA +25 -0
- kmodels-0.1.0.0.dist-info/RECORD +17 -0
- kmodels-0.1.0.0.dist-info/WHEEL +5 -0
- kmodels-0.1.0.0.dist-info/licenses/LICENSE +21 -0
- kmodels-0.1.0.0.dist-info/top_level.txt +1 -0
kmodels/__init__.py
ADDED
|
File without changes
|
kmodels/_old/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, ClassVar, Type, Iterable, TypeVar, Generic, Self
|
|
4
|
+
from pydantic import BaseModel, model_validator, Field, model_serializer, PlainSerializer
|
|
5
|
+
from typeguard import typechecked
|
|
6
|
+
from abc import abstractmethod, ABC
|
|
7
|
+
from lib.kmodels.error import IsAbstract
|
|
8
|
+
from lib.kmodels.types import OmitIfNone
|
|
9
|
+
from lib.kmodels.utils import AbstractUtils
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def deserialize_single(data: Any) -> Any:
|
|
13
|
+
return CoreModel.deserializer_helper(data)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def deserialize_dict(data: Any, *, key: bool, value: bool, generator: Any = dict) -> Any:
|
|
17
|
+
"""
|
|
18
|
+
Función auxiliar que deserializa un diccionario (o algo que se comporta como él)
|
|
19
|
+
:param data:
|
|
20
|
+
:param key:
|
|
21
|
+
:param value:
|
|
22
|
+
:param generator:
|
|
23
|
+
:return:
|
|
24
|
+
"""
|
|
25
|
+
if not key and not value:
|
|
26
|
+
return data
|
|
27
|
+
|
|
28
|
+
deserialized = (
|
|
29
|
+
(deserialize_single(k) if key else k, deserialize_single(v) if value else v,) for k, v in data.items()
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if generator is not None:
|
|
33
|
+
return generator(deserialized)
|
|
34
|
+
return deserialized
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def deserialize_iterable(
|
|
38
|
+
data: Iterable, *,
|
|
39
|
+
generator: Any = tuple
|
|
40
|
+
) -> Any:
|
|
41
|
+
deserialized = (deserialize_single(item) for item in data)
|
|
42
|
+
if generator is not None:
|
|
43
|
+
return generator(deserialized)
|
|
44
|
+
return deserialized
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class CoreModel(BaseModel):
|
|
48
|
+
__class_registry__: ClassVar[dict[str, Type[CoreModel]]] = {}
|
|
49
|
+
__auto_register__: ClassVar[bool] = False
|
|
50
|
+
|
|
51
|
+
__cls_key_name__: ClassVar[str] = 'cls_key'
|
|
52
|
+
cls_key: OmitIfNone[str | None] = Field(default=None)
|
|
53
|
+
|
|
54
|
+
@model_serializer
|
|
55
|
+
def _no_serialize_exclude_if_none(self):
|
|
56
|
+
skip_if_none = set()
|
|
57
|
+
serialize_aliases = dict()
|
|
58
|
+
|
|
59
|
+
# Gather fields that should omit if None
|
|
60
|
+
for name, field_info in self.model_fields.items():
|
|
61
|
+
# noinspection PyTypeHints
|
|
62
|
+
if any(
|
|
63
|
+
isinstance(metadata, OmitIfNone) for metadata in field_info.metadata
|
|
64
|
+
):
|
|
65
|
+
skip_if_none.add(name)
|
|
66
|
+
elif field_info.serialization_alias:
|
|
67
|
+
serialize_aliases[name] = field_info.serialization_alias
|
|
68
|
+
|
|
69
|
+
serialized = dict()
|
|
70
|
+
for name, value in self:
|
|
71
|
+
# Skip serializing None if it was marked with "OmitIfNone"
|
|
72
|
+
if value is None and name in skip_if_none:
|
|
73
|
+
continue
|
|
74
|
+
serialize_key = serialize_aliases.get(name, name)
|
|
75
|
+
|
|
76
|
+
# Run Annotated PlainSerializer
|
|
77
|
+
for metadata in self.model_fields[name].metadata:
|
|
78
|
+
if isinstance(metadata, PlainSerializer):
|
|
79
|
+
value = metadata.func(value)
|
|
80
|
+
|
|
81
|
+
serialized[serialize_key] = value
|
|
82
|
+
|
|
83
|
+
return serialized
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def __pydantic_init_subclass__(cls, **kwargs):
|
|
87
|
+
super().__pydantic_init_subclass__(**kwargs)
|
|
88
|
+
if cls.__auto_register__ and not cls.is_registered():
|
|
89
|
+
cls.register()
|
|
90
|
+
|
|
91
|
+
@model_validator(mode="before")
|
|
92
|
+
@classmethod
|
|
93
|
+
def _type_key_handler(cls, data: Any) -> Any:
|
|
94
|
+
if isinstance(data, dict):
|
|
95
|
+
# Autoasignar type_key si la clase está registrada y type_key es None
|
|
96
|
+
type_key = data.get(cls.__cls_key_name__)
|
|
97
|
+
|
|
98
|
+
# Autoasignamos type_key si no está definido
|
|
99
|
+
if type_key is None:
|
|
100
|
+
type_key = cls.generate_type_key()
|
|
101
|
+
if type_key in cls.__class_registry__:
|
|
102
|
+
data[cls.__cls_key_name__] = type_key
|
|
103
|
+
|
|
104
|
+
elif type_key not in cls.__class_registry__:
|
|
105
|
+
# Si type_key está definido entonces validamos que la clase esté registrada
|
|
106
|
+
raise ValueError(f"La clase '{cls.__cls_key_name__}' no está registrada.")
|
|
107
|
+
return data
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def is_registered(cls, type_key: str | None = None) -> bool:
|
|
111
|
+
if type_key is None:
|
|
112
|
+
type_key = cls.generate_type_key()
|
|
113
|
+
return type_key in cls.__class_registry__
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def _register_single(cls, target_class: Type[CoreModel]):
|
|
117
|
+
type_key = target_class.generate_type_key()
|
|
118
|
+
if type_key in cls.__class_registry__:
|
|
119
|
+
raise KeyError(f"La clase '{type_key}' ya está registrada.")
|
|
120
|
+
cls.__class_registry__[type_key] = target_class
|
|
121
|
+
|
|
122
|
+
@typechecked
|
|
123
|
+
@classmethod
|
|
124
|
+
def register(cls, target_classes: Type[CoreModel] | Iterable[Type[CoreModel]] | None = None) -> None:
|
|
125
|
+
"""
|
|
126
|
+
Registra la clase manualmente en el class_registry.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
name (str | None): Nombre opcional para registrar la clase. Si es None, se usa el nombre de la clase.
|
|
130
|
+
target_classes (Type[CoreModel] | None): Clase a registrar, si no se especifica se registrará esta clase.
|
|
131
|
+
"""
|
|
132
|
+
if target_classes is None:
|
|
133
|
+
target_classes = (cls,)
|
|
134
|
+
elif isinstance(target_classes, CoreModel):
|
|
135
|
+
target_classes = (target_classes,)
|
|
136
|
+
|
|
137
|
+
for tgt_class in target_classes:
|
|
138
|
+
cls._register_single(tgt_class)
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def get_registered_class(cls, type_key: str) -> Type[CoreModel]:
|
|
142
|
+
"""
|
|
143
|
+
Obtiene la clase registrada a partir del nombre de clase. Revisar __pydantic_init_subclass__ para más
|
|
144
|
+
información.
|
|
145
|
+
"""
|
|
146
|
+
if not cls.is_registered(type_key):
|
|
147
|
+
raise ValueError(f"Unknown or missing class_name: {type_key}")
|
|
148
|
+
target_cls = cls.__class_registry__[type_key]
|
|
149
|
+
|
|
150
|
+
if AbstractUtils.is_abstract(target_cls):
|
|
151
|
+
raise IsAbstract(f"Cannot use abstract class {type_key} directly.")
|
|
152
|
+
return target_cls
|
|
153
|
+
|
|
154
|
+
@classmethod
|
|
155
|
+
def generate_type_key(cls) -> str:
|
|
156
|
+
type_params = getattr(cls, '__pydantic_generic_metadata__', {}).get('args', ())
|
|
157
|
+
if type_params:
|
|
158
|
+
return f"{cls.__name__}[{','.join(tp.__name__ for tp in type_params)}]"
|
|
159
|
+
return cls.__name__
|
|
160
|
+
|
|
161
|
+
@classmethod
|
|
162
|
+
def deserializer_helper(cls, data: Any) -> Any:
|
|
163
|
+
if isinstance(data, dict):
|
|
164
|
+
cls_key = data.get(cls.__cls_key_name__)
|
|
165
|
+
if cls_key is not None:
|
|
166
|
+
target_cls = cls.__class_registry__[cls_key]
|
|
167
|
+
return target_cls(**data)
|
|
168
|
+
return data
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
CoreModelT = TypeVar('CoreModelT', bound=CoreModel)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class Modelable(Generic[CoreModelT], ABC):
|
|
175
|
+
@abstractmethod
|
|
176
|
+
def to_model(self) -> CoreModelT:
|
|
177
|
+
pass
|
|
178
|
+
|
|
179
|
+
@classmethod
|
|
180
|
+
@abstractmethod
|
|
181
|
+
def from_model(cls, model: CoreModelT) -> Self:
|
|
182
|
+
...
|
kmodels/error.py
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import abstractmethod, ABC
|
|
4
|
+
from typing import Any, ClassVar, Type, Iterable, TypeVar
|
|
5
|
+
from pydantic import BaseModel, model_validator, Field, model_serializer, PlainSerializer
|
|
6
|
+
from typeguard import typechecked
|
|
7
|
+
|
|
8
|
+
from kmodels.types import OmitIfNone
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class _CustomSerializator(BaseModel, ABC):
|
|
12
|
+
def _get_skip_if_none_fields(self) -> set[str]:
|
|
13
|
+
"""
|
|
14
|
+
Averigua que fields deben ser omitidos si son None por ser OmitIfNone
|
|
15
|
+
"""
|
|
16
|
+
return {
|
|
17
|
+
name
|
|
18
|
+
for name, field_info in self.model_fields.items()
|
|
19
|
+
if any(isinstance(metadata, OmitIfNone) for metadata in field_info.metadata)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
def _get_serialize_aliases(self) -> dict[str, str]:
|
|
23
|
+
"""
|
|
24
|
+
Crea un diccionario de alias a partir de self.model_fields()
|
|
25
|
+
"""
|
|
26
|
+
return {
|
|
27
|
+
name: field_info.serialization_alias
|
|
28
|
+
for name, field_info in self.model_fields.items()
|
|
29
|
+
if field_info.serialization_alias
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@model_serializer
|
|
33
|
+
def _core_serializer(self) -> dict[str, Any]:
|
|
34
|
+
"""
|
|
35
|
+
Serializador personalizado que omite los miembros OmitIfNone si es que son None
|
|
36
|
+
"""
|
|
37
|
+
skip_if_none = self._get_skip_if_none_fields()
|
|
38
|
+
serialize_aliases = self._get_serialize_aliases()
|
|
39
|
+
|
|
40
|
+
serialized = dict()
|
|
41
|
+
for name, value in self:
|
|
42
|
+
# Skip serializing None if it was marked with "OmitIfNone"
|
|
43
|
+
if value is None and name in skip_if_none:
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
serialize_key = serialize_aliases.get(name, name)
|
|
47
|
+
|
|
48
|
+
# Run Annotated PlainSerializer
|
|
49
|
+
for metadata in self.model_fields[name].metadata:
|
|
50
|
+
if isinstance(metadata, PlainSerializer):
|
|
51
|
+
value = metadata.func(value)
|
|
52
|
+
|
|
53
|
+
serialized[serialize_key] = value
|
|
54
|
+
|
|
55
|
+
return serialized
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class _PrivateCoreModel(_CustomSerializator, ABC):
|
|
59
|
+
__class_registry__: ClassVar[dict[str, Type[CoreModel]]] = {}
|
|
60
|
+
__auto_register__: ClassVar[bool] = False
|
|
61
|
+
|
|
62
|
+
__cls_key_name__: ClassVar[str] = 'cls_key'
|
|
63
|
+
cls_key: OmitIfNone[str | None] = Field(default=None)
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def __pydantic_init_subclass__(cls, **kwargs):
|
|
67
|
+
"""
|
|
68
|
+
Incorpora la funcionalidad de registrar las subclases automáticamente si __auto_register__ == True.
|
|
69
|
+
Si no se quiere/puede utilizar en una rama de clases entonces puedes usar CoreModel.register(CLASE) o
|
|
70
|
+
simplemente CLASE.register() con las clases que lo requieran.
|
|
71
|
+
"""
|
|
72
|
+
super().__pydantic_init_subclass__(**kwargs)
|
|
73
|
+
if cls.__auto_register__ and not cls.is_registered():
|
|
74
|
+
cls.register()
|
|
75
|
+
|
|
76
|
+
@model_validator(mode="before")
|
|
77
|
+
@classmethod
|
|
78
|
+
def _cls_key_handler(cls, data: Any) -> Any:
|
|
79
|
+
"""
|
|
80
|
+
Si la clase está registrada:
|
|
81
|
+
- Asigna automáticamente cls_key.
|
|
82
|
+
Si la clase no está registrada:
|
|
83
|
+
- Lanza excepción si se ha indicado cls_key.
|
|
84
|
+
- Lanza excepción si el cls_key
|
|
85
|
+
"""
|
|
86
|
+
if isinstance(data, dict):
|
|
87
|
+
# Autoasignar cls_key si la clase está registrada y cls_key es None
|
|
88
|
+
cls_key = data.get(cls.__cls_key_name__)
|
|
89
|
+
valid_cls_key = cls.generate_cls_key()
|
|
90
|
+
|
|
91
|
+
# Si data no contiene cls_key lo deducimos para averiguar si la clase está registrada
|
|
92
|
+
if cls_key is None:
|
|
93
|
+
# Si está registrada entonces agregamos el cls_key a la data
|
|
94
|
+
if cls.is_registered(valid_cls_key):
|
|
95
|
+
data[cls.__cls_key_name__] = valid_cls_key
|
|
96
|
+
return data
|
|
97
|
+
|
|
98
|
+
# data contiene cls_key (no es None)
|
|
99
|
+
# El cls_key debe ser válido
|
|
100
|
+
if cls_key != valid_cls_key:
|
|
101
|
+
raise ValueError(
|
|
102
|
+
f"El cls_key indicado no es válido. Trata de no asignar este valor manualmente "
|
|
103
|
+
f"(cls_key={cls_key}, valid_cls_key={valid_cls_key})\n\t"
|
|
104
|
+
)
|
|
105
|
+
# La clase debe estar registrada
|
|
106
|
+
if not cls.is_registered(cls_key):
|
|
107
|
+
# Si type_key está definido entonces validamos que la clase esté registrada
|
|
108
|
+
raise ValueError(f"La clase '{cls_key}' no está registrada.")
|
|
109
|
+
|
|
110
|
+
return data
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def _register_single(cls, target_class: Type[CoreModel]):
|
|
114
|
+
"""
|
|
115
|
+
Registra target_class.
|
|
116
|
+
"""
|
|
117
|
+
cls_key = target_class.generate_cls_key()
|
|
118
|
+
if cls_key in cls.__class_registry__:
|
|
119
|
+
raise KeyError(f"La clase '{cls_key}' ya está registrada.")
|
|
120
|
+
cls.__class_registry__[cls_key] = target_class
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
@abstractmethod
|
|
124
|
+
def register(cls, target_classes: Type[CoreModel] | Iterable[Type[CoreModel]] | None = None) -> None:
|
|
125
|
+
...
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
@abstractmethod
|
|
129
|
+
def is_registered(cls, cls_key: str | None = None) -> bool:
|
|
130
|
+
...
|
|
131
|
+
|
|
132
|
+
@classmethod
|
|
133
|
+
@abstractmethod
|
|
134
|
+
def generate_cls_key(cls) -> str:
|
|
135
|
+
...
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class CoreModel(_PrivateCoreModel):
|
|
139
|
+
@classmethod
|
|
140
|
+
def is_registered(cls, cls_key: str | None = None) -> bool:
|
|
141
|
+
"""
|
|
142
|
+
Retorna True si la clase está registrada.
|
|
143
|
+
(TIP: Puedes obtener cls_key mediante Clase.cls_key)
|
|
144
|
+
"""
|
|
145
|
+
if cls_key is None:
|
|
146
|
+
cls_key = cls.generate_cls_key()
|
|
147
|
+
return cls_key in cls.__class_registry__
|
|
148
|
+
|
|
149
|
+
@typechecked
|
|
150
|
+
@classmethod
|
|
151
|
+
def register(cls, target_classes: Type[CoreModel] | Iterable[Type[CoreModel]] | None = None) -> None:
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
"""
|
|
155
|
+
if target_classes is None:
|
|
156
|
+
target_classes = (cls,)
|
|
157
|
+
elif isinstance(target_classes, CoreModel):
|
|
158
|
+
target_classes = (target_classes,)
|
|
159
|
+
|
|
160
|
+
for tgt_class in target_classes:
|
|
161
|
+
cls._register_single(tgt_class)
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def get_registered_class(cls, cls_key: str) -> Type[CoreModel]:
|
|
165
|
+
"""
|
|
166
|
+
Obtiene la clase asociada al cls_key o lanza excepción KeyError en caso de no encontrarse.
|
|
167
|
+
(TIP: Puedes obtener cls_key mediante Clase.cls_key)
|
|
168
|
+
"""
|
|
169
|
+
if not cls.is_registered(cls_key):
|
|
170
|
+
raise KeyError(f"Unknown or missing class_name: {cls_key}")
|
|
171
|
+
target_cls = cls.__class_registry__[cls_key]
|
|
172
|
+
|
|
173
|
+
# Puede que sea necesario sino remover en un futuro
|
|
174
|
+
# if AbstractUtils.is_abstract(target_cls):
|
|
175
|
+
# raise IsAbstract(f"Cannot use abstract class {cls_key} directly.")
|
|
176
|
+
return target_cls
|
|
177
|
+
|
|
178
|
+
@classmethod
|
|
179
|
+
def generate_cls_key(cls) -> str:
|
|
180
|
+
"""
|
|
181
|
+
Genera una key única para cada clase que va a funcionar incluso con tipos genéricos.
|
|
182
|
+
"""
|
|
183
|
+
type_params = getattr(cls, '__pydantic_generic_metadata__', {}).get('args', ())
|
|
184
|
+
if type_params:
|
|
185
|
+
return f"{cls.__name__}[{','.join(tp.__name__ for tp in type_params)}]"
|
|
186
|
+
return cls.__name__
|
|
187
|
+
|
|
188
|
+
@classmethod
|
|
189
|
+
def polymorphic_single(cls, data: Any) -> Any:
|
|
190
|
+
"""
|
|
191
|
+
Función auxiliar de los model_validator del usuario. Se debe usar con un miembro polimórfico como por ejemplo
|
|
192
|
+
animal: Animal, y si se ha registrado previamente 'Gato' y 'Perro' y los datos se corresponden con los de un
|
|
193
|
+
'Gato' entonces se usará el registro de clases para averiguar la clase correcta y construir un objeto Gato.
|
|
194
|
+
|
|
195
|
+
users_dict: SerializeAsAny[dict[str, User]]
|
|
196
|
+
|
|
197
|
+
...
|
|
198
|
+
|
|
199
|
+
@model_validator(mode="before")
|
|
200
|
+
@classmethod
|
|
201
|
+
def _handle_polymorphic_fields(cls, data: Any) -> dict[str, Any]:
|
|
202
|
+
if isinstance(data, dict):
|
|
203
|
+
field_name = 'mascota'
|
|
204
|
+
_mascota = data.get(field_name)
|
|
205
|
+
if _mascota is not None:
|
|
206
|
+
data[field_name] = cls.polymorphic_single(_mascota)
|
|
207
|
+
return data
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
if isinstance(data, dict):
|
|
211
|
+
cls_key = data.get(cls.__cls_key_name__)
|
|
212
|
+
if cls_key is not None:
|
|
213
|
+
target_cls = cls.__class_registry__[cls_key]
|
|
214
|
+
return target_cls(**data)
|
|
215
|
+
return data
|
|
216
|
+
|
|
217
|
+
@classmethod
|
|
218
|
+
def polymorphic_iterable(cls, data: Iterable, *, generator: Any = tuple) -> Any:
|
|
219
|
+
"""
|
|
220
|
+
Función auxiliar de los model_validator del usuario. Similar a polymorphic_single, pero ayuda con los iterables
|
|
221
|
+
en general. Debes usar generator para especificar el tipo de salida (eg. list, tuple, etc.).
|
|
222
|
+
"""
|
|
223
|
+
deserialized = (cls.polymorphic_single(item) for item in data)
|
|
224
|
+
if generator is not None:
|
|
225
|
+
return generator(deserialized)
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
@classmethod
|
|
229
|
+
def polymorphic_dict(cls, data: Any, *, key: bool = False, value: bool = False, generator: Any = dict) -> Any:
|
|
230
|
+
"""
|
|
231
|
+
Función auxiliar de los model_validator del usuario. Similar a polymorphic_single, pero ayuda con los
|
|
232
|
+
diccionarios en lugar de objetos sencillos. Hay que indicar que es polimórfico si el key, el value o ambos,
|
|
233
|
+
y el tipo que se quiere construir (ej. dict, OrderedDict).
|
|
234
|
+
|
|
235
|
+
Esta función llamará a polymorphic_single con todas las keys/valores (dependiendo de lo que sea polimórfico)
|
|
236
|
+
y finalmente retornará el tipo indicado en generator.
|
|
237
|
+
|
|
238
|
+
class ContainerOfAbstractContainers(CoreModel):
|
|
239
|
+
abs_field: SerializeAsAny[SampleContainerBase]
|
|
240
|
+
dict_fields: SerializeAsAny[dict[str, SampleContainerBase]]
|
|
241
|
+
list_field: SerializeAsAny[list[SampleBaseClass]]
|
|
242
|
+
tuple_field: SerializeAsAny[tuple[SampleBaseClass, ...]]
|
|
243
|
+
|
|
244
|
+
@model_validator(mode="before")
|
|
245
|
+
@classmethod
|
|
246
|
+
def validate_fields(cls, data: Any) -> Any:
|
|
247
|
+
if isinstance(data, dict):
|
|
248
|
+
deserialization_map = {
|
|
249
|
+
'abs_field': cls.polymorphic_single,
|
|
250
|
+
'dict_fields': lambda d: cls.polymorphic_dict(d, key=False, value=True, generator=dict),
|
|
251
|
+
'list_field': lambda d: cls.polymorphic_iterable(d or [], generator=list),
|
|
252
|
+
'tuple_field': lambda d: cls.polymorphic_iterable(d or (), generator=tuple),
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
for field, func in deserialization_map.items():
|
|
256
|
+
if field in data:
|
|
257
|
+
data[field] = func(data.get(field))
|
|
258
|
+
|
|
259
|
+
return data
|
|
260
|
+
"""
|
|
261
|
+
if not key and not value:
|
|
262
|
+
return data
|
|
263
|
+
|
|
264
|
+
deserialized = (
|
|
265
|
+
(
|
|
266
|
+
cls.polymorphic_single(k) if key else k,
|
|
267
|
+
cls.polymorphic_single(v) if value else v,
|
|
268
|
+
) for k, v in data.items()
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
if generator is not None:
|
|
272
|
+
return generator(deserialized)
|
|
273
|
+
return deserialized
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
CoreModelT = TypeVar('CoreModelT', bound=CoreModel)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from typing import final, Literal
|
|
2
|
+
|
|
3
|
+
from kmodels.models.coremodel import CoreModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@final
|
|
7
|
+
class Unset(CoreModel):
|
|
8
|
+
discriminator: Literal['Unset'] = 'Unset'
|
|
9
|
+
|
|
10
|
+
def __repr__(self) -> str:
|
|
11
|
+
return "<unset>"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@final
|
|
15
|
+
class Leave(CoreModel):
|
|
16
|
+
discriminator: Literal['Leave'] = 'Leave'
|
|
17
|
+
|
|
18
|
+
def __repr__(self) -> str:
|
|
19
|
+
return "<leave>"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
unset = Unset()
|
|
23
|
+
leave = Leave()
|
kmodels/test/__init__.py
ADDED
|
File without changes
|
kmodels/test/test.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import SerializeAsAny, model_validator
|
|
6
|
+
from kmodels.models.coremodel import CoreModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def complex_test():
|
|
10
|
+
# TypeModel.__auto_register__ = True
|
|
11
|
+
|
|
12
|
+
class SampleBaseClass(CoreModel):
|
|
13
|
+
# __auto_register__: ClassVar[bool] = True
|
|
14
|
+
...
|
|
15
|
+
|
|
16
|
+
class SampleDerivedA(SampleBaseClass):
|
|
17
|
+
...
|
|
18
|
+
|
|
19
|
+
class SampleContainerBase(CoreModel):
|
|
20
|
+
# __auto_register__: ClassVar[bool] = True
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
class SampleContainerA(SampleContainerBase):
|
|
24
|
+
abs_field: SerializeAsAny[SampleBaseClass]
|
|
25
|
+
|
|
26
|
+
@model_validator(mode="before")
|
|
27
|
+
@classmethod
|
|
28
|
+
def validate_fields(cls, data: Any) -> Any:
|
|
29
|
+
if isinstance(data, dict):
|
|
30
|
+
data['abs_field'] = cls.polymorphic_single(data.get('abs_field'))
|
|
31
|
+
return data
|
|
32
|
+
|
|
33
|
+
class ContainerOfAbstractContainers(CoreModel):
|
|
34
|
+
abs_field: SerializeAsAny[SampleContainerBase]
|
|
35
|
+
dict_fields: SerializeAsAny[dict[str, SampleContainerBase]]
|
|
36
|
+
list_field: SerializeAsAny[list[SampleBaseClass]]
|
|
37
|
+
tuple_field: SerializeAsAny[tuple[SampleBaseClass, ...]]
|
|
38
|
+
|
|
39
|
+
@model_validator(mode="before")
|
|
40
|
+
@classmethod
|
|
41
|
+
def validate_fields(cls, data: Any) -> Any:
|
|
42
|
+
if isinstance(data, dict):
|
|
43
|
+
deserialization_map = {
|
|
44
|
+
'abs_field': cls.polymorphic_single,
|
|
45
|
+
'dict_fields': lambda d: cls.polymorphic_dict(d, key=False, value=True, generator=dict),
|
|
46
|
+
'list_field': lambda d: cls.polymorphic_iterable(d or [], generator=list),
|
|
47
|
+
'tuple_field': lambda d: cls.polymorphic_iterable(d or (), generator=tuple),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for field, func in deserialization_map.items():
|
|
51
|
+
if field in data:
|
|
52
|
+
data[field] = func(data.get(field))
|
|
53
|
+
|
|
54
|
+
return data
|
|
55
|
+
|
|
56
|
+
CoreModel.register((SampleDerivedA, SampleContainerA))
|
|
57
|
+
|
|
58
|
+
print('__class_registry__:', CoreModel.__class_registry__)
|
|
59
|
+
|
|
60
|
+
print('1. Jerarquía sencilla sin muchas complicaciones')
|
|
61
|
+
derived_a = SampleDerivedA()
|
|
62
|
+
container_a = SampleContainerA(abs_field=derived_a)
|
|
63
|
+
print(container_a)
|
|
64
|
+
|
|
65
|
+
dumped = container_a.model_dump()
|
|
66
|
+
print(dumped)
|
|
67
|
+
|
|
68
|
+
cc2 = SampleContainerA.model_validate(dumped)
|
|
69
|
+
print(cc2)
|
|
70
|
+
|
|
71
|
+
print('\n2. Jerarquía anidada con nuevos campos')
|
|
72
|
+
super_container = ContainerOfAbstractContainers(
|
|
73
|
+
abs_field=container_a,
|
|
74
|
+
dict_fields={'test': container_a, 'test2': container_a},
|
|
75
|
+
list_field=[derived_a, derived_a],
|
|
76
|
+
tuple_field=(derived_a, derived_a),
|
|
77
|
+
)
|
|
78
|
+
print(super_container)
|
|
79
|
+
|
|
80
|
+
dumped = super_container.model_dump()
|
|
81
|
+
print(dumped)
|
|
82
|
+
|
|
83
|
+
super_container2 = ContainerOfAbstractContainers.model_validate(dumped)
|
|
84
|
+
print(super_container2)
|
|
85
|
+
|
|
86
|
+
print(CoreModel.__class_registry__)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def simple_test():
|
|
90
|
+
class User(CoreModel):
|
|
91
|
+
name: str
|
|
92
|
+
|
|
93
|
+
class DiscordUser(User):
|
|
94
|
+
description: str
|
|
95
|
+
|
|
96
|
+
class UserBundle(CoreModel):
|
|
97
|
+
users: tuple[User, ...]
|
|
98
|
+
users_dict: SerializeAsAny[dict[str, User]]
|
|
99
|
+
|
|
100
|
+
@model_validator(mode="before")
|
|
101
|
+
@classmethod
|
|
102
|
+
def _handle_polymorphic_fields(cls, data: Any) -> Any:
|
|
103
|
+
field_name = 'users'
|
|
104
|
+
_users = data.get(field_name)
|
|
105
|
+
if _users is not None:
|
|
106
|
+
data[field_name] = cls.polymorphic_iterable(_users, generator=tuple)
|
|
107
|
+
|
|
108
|
+
field_name = 'users_dict'
|
|
109
|
+
_users_dict = data.get(field_name)
|
|
110
|
+
if _users_dict is not None:
|
|
111
|
+
data[field_name] = cls.polymorphic_dict(_users_dict, value=True, generator=dict)
|
|
112
|
+
|
|
113
|
+
return data
|
|
114
|
+
|
|
115
|
+
DiscordUser.register()
|
|
116
|
+
|
|
117
|
+
user = DiscordUser(
|
|
118
|
+
name='火星「マールス」',
|
|
119
|
+
description='''Developer
|
|
120
|
+
Computer science student
|
|
121
|
+
日本語を勉強してる'''
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
users_dict = {'kasei': user}
|
|
125
|
+
|
|
126
|
+
user_bundle = UserBundle(users=(user,), users_dict=users_dict)
|
|
127
|
+
print('user_bundle', user_bundle)
|
|
128
|
+
json_data = user_bundle.model_dump_json(indent=2)
|
|
129
|
+
print('json_data', json_data)
|
|
130
|
+
rebuild_of_bundle = UserBundle.model_validate_json(json_data)
|
|
131
|
+
print('rebuild_of_bundle', rebuild_of_bundle)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
if __name__ == '__main__':
|
|
135
|
+
# complex_test()
|
|
136
|
+
simple_test()
|
kmodels/types.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from typing import TypeVar, TYPE_CHECKING, Annotated, Any
|
|
2
|
+
|
|
3
|
+
AnyType = TypeVar("AnyType")
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
OmitIfNone = Annotated[AnyType, ...]
|
|
7
|
+
else:
|
|
8
|
+
class OmitIfNone:
|
|
9
|
+
def __class_getitem__(cls, item: Any) -> Any:
|
|
10
|
+
return Annotated[item, OmitIfNone()]
|
kmodels/utils.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from abc import ABC
|
|
5
|
+
from typing import Type
|
|
6
|
+
|
|
7
|
+
from kmodels.error import IsAbstract
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Utilidad para manejar clases abstractas
|
|
11
|
+
class AbstractUtils:
|
|
12
|
+
@classmethod
|
|
13
|
+
def is_abstract(cls, target_class: Type):
|
|
14
|
+
return inspect.isabstract(target_class) or ABC in target_class.__bases__
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def raise_abstract_class(cls, target_class: Type):
|
|
18
|
+
if cls.is_abstract(target_class):
|
|
19
|
+
cname = target_class.__name__
|
|
20
|
+
raise IsAbstract(f"{cname} is an abstract class (inherits from ABC) and you cannot instantiate it.")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# # Utilidad para manejar la validación de class_name
|
|
24
|
+
# class ClassNameUtils:
|
|
25
|
+
# @classmethod
|
|
26
|
+
# def validate_class_name(cls, target_class: Type):
|
|
27
|
+
# # Detectar si es una clase genérica intermedia generada por Pydantic
|
|
28
|
+
# is_generic_intermediate = '[' in target_class.__name__ and ']' in target_class.__name__
|
|
29
|
+
# if is_generic_intermediate:
|
|
30
|
+
# return
|
|
31
|
+
#
|
|
32
|
+
# # Si es una clase abstracta, no aplicar validación
|
|
33
|
+
# if cls.is_abstract(target_class):
|
|
34
|
+
# return
|
|
35
|
+
#
|
|
36
|
+
# # Validar la existencia y tipo de class_name
|
|
37
|
+
# class_name_annotation = target_class.__annotations__.get('class_name', None)
|
|
38
|
+
#
|
|
39
|
+
# if class_name_annotation is None:
|
|
40
|
+
# raise AttributeError(
|
|
41
|
+
# f'Si {target_class.__name__} es abstracta entonces debe heredar de ABC o definir al menos un método '
|
|
42
|
+
# f'abstracto, si no lo es entonces debes definirle el atributo class_name de la siguiente manera:\n'
|
|
43
|
+
# f'\tclass_name: Literal["{target_class.__name__}"] = "{target_class.__name__}"'
|
|
44
|
+
# )
|
|
45
|
+
#
|
|
46
|
+
# # noinspection PyUnresolvedReferences
|
|
47
|
+
# class_name_type = target_class.model_fields['class_name'].annotation
|
|
48
|
+
#
|
|
49
|
+
# origin_type = get_origin(class_name_type)
|
|
50
|
+
# if origin_type is not Literal:
|
|
51
|
+
# raise AttributeError(
|
|
52
|
+
# f"El atributo 'class_name' en la clase {target_class.__name__} debe ser un Literal con el valor "
|
|
53
|
+
# f"exacto del nombre de la clase.\n"
|
|
54
|
+
# f"\t(Definición de tipo esperada)\n\t\tclass_name: Literal['{target_class.__name__}'] = '{target_class.__name__}'\n"
|
|
55
|
+
# f"\t(Definición de tipo encontrada)\n\t\tclass_name: {class_name_annotation} ..."
|
|
56
|
+
# )
|
|
57
|
+
#
|
|
58
|
+
# literal_args = list(get_args(class_name_type))
|
|
59
|
+
# if len(literal_args) != 1:
|
|
60
|
+
# raise AttributeError(
|
|
61
|
+
# f"El atributo 'class_name' en la clase {target_class.__name__} debe ser un Literal con un único valor,\n"
|
|
62
|
+
# f"pero se encontraron múltiples opciones: {literal_args}\n"
|
|
63
|
+
# f"\t(Definición esperada)\n\t\tclass_name: "
|
|
64
|
+
# f"Literal['{target_class.__name__}'] = '{target_class.__name__}'\n"
|
|
65
|
+
# f"\t(Definición encontrada)\n\t\tclass_name: Literal{literal_args}"
|
|
66
|
+
# )
|
|
67
|
+
#
|
|
68
|
+
# if literal_args[0] != target_class.__name__:
|
|
69
|
+
# raise AttributeError(
|
|
70
|
+
# f"El atributo 'class_name' en la clase {target_class.__name__} debe ser un Literal con el valor exacto de su nombre.\n"
|
|
71
|
+
# f"\t(Definición esperada)\n\t\tclass_name: Literal['{target_class.__name__}'] = '{target_class.__name__}'\n"
|
|
72
|
+
# f"\t(Definición encontrada)\n\t\tclass_name: Literal['{literal_args[0]}'] = '{literal_args[0]}'"
|
|
73
|
+
# )
|
|
74
|
+
#
|
|
75
|
+
# @classmethod
|
|
76
|
+
# def is_abstract(cls, target_class: Type):
|
|
77
|
+
# return AbstractUtils.is_abstract(target_class)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kmodels
|
|
3
|
+
Version: 0.1.0.0
|
|
4
|
+
Summary: KModels
|
|
5
|
+
Home-page: https://github.com/kokaito-git/knotification
|
|
6
|
+
Author: kokaito-git
|
|
7
|
+
Author-email: kokaito.git@gmail.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
11
|
+
Classifier: Operating System :: MacOS
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Requires-Python: >=3.13
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: pydantic
|
|
17
|
+
Requires-Dist: typeguard
|
|
18
|
+
Dynamic: author
|
|
19
|
+
Dynamic: author-email
|
|
20
|
+
Dynamic: classifier
|
|
21
|
+
Dynamic: home-page
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
Dynamic: requires-dist
|
|
24
|
+
Dynamic: requires-python
|
|
25
|
+
Dynamic: summary
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
kmodels/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
kmodels/error.py,sha256=9VbKUbFA_R_YcuhYNymyFHwzsEiBuvBIHwlc17F0eKw,38
|
|
3
|
+
kmodels/types.py,sha256=1a8G2CSxJcskBFhgGjyZeYuSK1ctB0CRqgAwvWrY6SE,279
|
|
4
|
+
kmodels/utils.py,sha256=H5Z_scnL30UQssS3FU3I-tjRGCWntbWaBh5s150WCLA,3602
|
|
5
|
+
kmodels/_old/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
kmodels/_old/_old_models.py,sha256=jeO3C9y1RL857xHORTQT2YjPcASLcz7kHEiwMPQwEoc,6377
|
|
7
|
+
kmodels/models/__init__.py,sha256=lyRGkUSjap79kOjy5n-q7XwU7laN3Sd3rMp0dJtr7Ck,121
|
|
8
|
+
kmodels/models/coremodel/__init__.py,sha256=AFakx3Ukc1dYR7N30hnYLCPM8gB4bU0RzJu6VDwZgV4,146
|
|
9
|
+
kmodels/models/coremodel/coremodel.py,sha256=ny44joj--97ft9G24see8Oub8ZABuQbLOzvIabR_HHg,10684
|
|
10
|
+
kmodels/models/coremodel/leave_unset.py,sha256=B6hH973B0jy0zhJZmtK7MEPPDc4SD34StnsiXUCtUqA,388
|
|
11
|
+
kmodels/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
kmodels/test/test.py,sha256=WqrVwpEbRuhxq7qQ9GKXweusS3QRU9jWcWzXyeC3uMU,4265
|
|
13
|
+
kmodels-0.1.0.0.dist-info/licenses/LICENSE,sha256=IMGJwRbv0HTEuPVu4apLyiXc1vyd7jpbIaUAw1J8S9c,1068
|
|
14
|
+
kmodels-0.1.0.0.dist-info/METADATA,sha256=CBedb2wkjVH4Jcdw6HsCM6sTdgo_jUbx-TboAYZdGiM,725
|
|
15
|
+
kmodels-0.1.0.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
16
|
+
kmodels-0.1.0.0.dist-info/top_level.txt,sha256=XeaAElJ1c9V1JGq7Lbl7IPc_OEnf40LsHmnO1tjbR20,8
|
|
17
|
+
kmodels-0.1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 kokaito-git
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
kmodels
|