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 ADDED
File without changes
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,3 @@
1
+
2
+ class IsAbstract(Exception):
3
+ ...
@@ -0,0 +1,5 @@
1
+ from .coremodel import CoreModel, CoreModelT, Leave, Unset
2
+
3
+ __all__ = [
4
+ 'CoreModel', 'CoreModelT', 'Leave', 'Unset'
5
+ ]
@@ -0,0 +1,6 @@
1
+ from .coremodel import CoreModel, CoreModelT
2
+ from .leave_unset import Leave, Unset
3
+
4
+ __all__ = [
5
+ 'CoreModel', 'CoreModelT', 'Leave', 'Unset'
6
+ ]
@@ -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()
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (78.1.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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