hammad-python 0.0.10__py3-none-any.whl → 0.0.11__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.
Files changed (74) hide show
  1. hammad/__init__.py +64 -10
  2. hammad/based/__init__.py +52 -0
  3. hammad/based/fields.py +546 -0
  4. hammad/based/model.py +968 -0
  5. hammad/based/utils.py +455 -0
  6. hammad/cache/__init__.py +30 -0
  7. hammad/{cache.py → cache/_cache.py} +83 -12
  8. hammad/cli/__init__.py +25 -0
  9. hammad/cli/plugins/__init__.py +786 -0
  10. hammad/cli/styles/__init__.py +5 -0
  11. hammad/cli/styles/animations.py +548 -0
  12. hammad/cli/styles/settings.py +135 -0
  13. hammad/cli/styles/types.py +358 -0
  14. hammad/cli/styles/utils.py +480 -0
  15. hammad/data/__init__.py +51 -0
  16. hammad/data/collections/__init__.py +32 -0
  17. hammad/data/collections/base_collection.py +58 -0
  18. hammad/data/collections/collection.py +227 -0
  19. hammad/data/collections/searchable_collection.py +556 -0
  20. hammad/data/collections/vector_collection.py +497 -0
  21. hammad/data/databases/__init__.py +21 -0
  22. hammad/data/databases/database.py +551 -0
  23. hammad/data/types/__init__.py +33 -0
  24. hammad/data/types/files/__init__.py +1 -0
  25. hammad/data/types/files/audio.py +81 -0
  26. hammad/data/types/files/configuration.py +475 -0
  27. hammad/data/types/files/document.py +195 -0
  28. hammad/data/types/files/file.py +358 -0
  29. hammad/data/types/files/image.py +80 -0
  30. hammad/json/__init__.py +21 -0
  31. hammad/{utils/json → json}/converters.py +4 -1
  32. hammad/logging/__init__.py +27 -0
  33. hammad/logging/decorators.py +432 -0
  34. hammad/logging/logger.py +534 -0
  35. hammad/pydantic/__init__.py +43 -0
  36. hammad/{utils/pydantic → pydantic}/converters.py +2 -1
  37. hammad/pydantic/models/__init__.py +28 -0
  38. hammad/pydantic/models/arbitrary_model.py +46 -0
  39. hammad/pydantic/models/cacheable_model.py +79 -0
  40. hammad/pydantic/models/fast_model.py +318 -0
  41. hammad/pydantic/models/function_model.py +176 -0
  42. hammad/pydantic/models/subscriptable_model.py +63 -0
  43. hammad/text/__init__.py +37 -0
  44. hammad/text/text.py +1068 -0
  45. hammad/text/utils/__init__.py +1 -0
  46. hammad/{utils/text → text/utils}/converters.py +2 -2
  47. hammad/text/utils/markdown/__init__.py +1 -0
  48. hammad/{utils → text/utils}/markdown/converters.py +3 -3
  49. hammad/{utils → text/utils}/markdown/formatting.py +1 -1
  50. hammad/{utils/typing/utils.py → typing/__init__.py} +75 -2
  51. hammad/web/__init__.py +42 -0
  52. hammad/web/http/__init__.py +1 -0
  53. hammad/web/http/client.py +944 -0
  54. hammad/web/openapi/client.py +740 -0
  55. hammad/web/search/__init__.py +1 -0
  56. hammad/web/search/client.py +936 -0
  57. hammad/web/utils.py +463 -0
  58. hammad/yaml/__init__.py +30 -0
  59. hammad/yaml/converters.py +19 -0
  60. {hammad_python-0.0.10.dist-info → hammad_python-0.0.11.dist-info}/METADATA +14 -8
  61. hammad_python-0.0.11.dist-info/RECORD +65 -0
  62. hammad/database.py +0 -447
  63. hammad/logger.py +0 -273
  64. hammad/types/color.py +0 -951
  65. hammad/utils/json/__init__.py +0 -0
  66. hammad/utils/markdown/__init__.py +0 -0
  67. hammad/utils/pydantic/__init__.py +0 -0
  68. hammad/utils/text/__init__.py +0 -0
  69. hammad/utils/typing/__init__.py +0 -0
  70. hammad_python-0.0.10.dist-info/RECORD +0 -22
  71. /hammad/{types/__init__.py → py.typed} +0 -0
  72. /hammad/{utils → web/openapi}/__init__.py +0 -0
  73. {hammad_python-0.0.10.dist-info → hammad_python-0.0.11.dist-info}/WHEEL +0 -0
  74. {hammad_python-0.0.10.dist-info → hammad_python-0.0.11.dist-info}/licenses/LICENSE +0 -0
hammad/based/model.py ADDED
@@ -0,0 +1,968 @@
1
+ """hammad.core.base.model"""
2
+
3
+ import copy
4
+ from functools import lru_cache
5
+ from typing import Any, Dict, List, Literal, Optional, Self, Set, Union
6
+
7
+ import msgspec
8
+ from msgspec.json import decode, encode, schema
9
+ from msgspec.structs import Struct, asdict, fields
10
+
11
+ __all__ = ("BasedModel",)
12
+
13
+
14
+ def _get_field_schema(field) -> dict[str, Any]:
15
+ """Helper method to generate schema for a single field."""
16
+ field_type = field.type
17
+
18
+ # Handle basic types
19
+ if field_type == str:
20
+ return {"type": "string"}
21
+ elif field_type == int:
22
+ return {"type": "integer"}
23
+ elif field_type == float:
24
+ return {"type": "number"}
25
+ elif field_type == bool:
26
+ return {"type": "boolean"}
27
+ elif field_type == list:
28
+ return {"type": "array"}
29
+ elif field_type == dict:
30
+ return {"type": "object"}
31
+
32
+ # Handle Optional types (Union with None)
33
+ if hasattr(field_type, "__origin__") and field_type.__origin__ is Union:
34
+ args = field_type.__args__
35
+ if len(args) == 2 and type(None) in args:
36
+ # This is Optional[T]
37
+ non_none_type = args[0] if args[1] is type(None) else args[1]
38
+ base_schema = _get_type_schema(non_none_type)
39
+ base_schema["nullable"] = True
40
+ return base_schema
41
+
42
+ # Handle generic types
43
+ if hasattr(field_type, "__origin__"):
44
+ origin = field_type.__origin__
45
+ if origin is list:
46
+ args = getattr(field_type, "__args__", ())
47
+ if args:
48
+ return {"type": "array", "items": _get_type_schema(args[0])}
49
+ return {"type": "array"}
50
+ elif origin is dict:
51
+ return {"type": "object"}
52
+ elif origin is set:
53
+ args = getattr(field_type, "__args__", ())
54
+ if args:
55
+ return {
56
+ "type": "array",
57
+ "items": _get_type_schema(args[0]),
58
+ "uniqueItems": True,
59
+ }
60
+ return {"type": "array", "uniqueItems": True}
61
+
62
+ # Default fallback
63
+ return {"type": "object"}
64
+
65
+
66
+ def _get_type_schema(type_hint) -> dict[str, Any]:
67
+ """Helper method to get schema for a type hint."""
68
+ if type_hint == str:
69
+ return {"type": "string"}
70
+ elif type_hint == int:
71
+ return {"type": "integer"}
72
+ elif type_hint == float:
73
+ return {"type": "number"}
74
+ elif type_hint == bool:
75
+ return {"type": "boolean"}
76
+ elif type_hint == list:
77
+ return {"type": "array"}
78
+ elif type_hint == dict:
79
+ return {"type": "object"}
80
+ elif type_hint == set:
81
+ return {"type": "array", "uniqueItems": True}
82
+ else:
83
+ return {"type": "object"}
84
+
85
+
86
+ class BasedModel(Struct):
87
+ """Based, as defined by Lil B is:
88
+
89
+ ```markdown
90
+ "Based means being yourself.
91
+ Not being scared of what people think about you.
92
+ Not being afraid to do what you wanna do."
93
+ ```
94
+
95
+ NOTE: This model does support the dictionary interface, but
96
+ does not support the `keys`, `values`, `items`, and `get`
97
+ methods to allow for usage of those fields as key names.
98
+
99
+ These wise words define this model. A `BasedModel` is
100
+ interactable both through the dictionary interface, and
101
+ dot notation, and utilizes `msgspec` to provide an interface
102
+ identical to a `pydantic.BaseModel`, with the benefit of
103
+ `msgspec's` superior performance (5-60x faster for common operations).
104
+ """
105
+
106
+ def __init_subclass__(cls, **kwargs):
107
+ """Called when a class is subclassed to set up proper typing."""
108
+ super().__init_subclass__(**kwargs)
109
+
110
+ # Create dynamic properties for field access to help with IDE completion
111
+ try:
112
+ struct_fields = fields(cls)
113
+ field_names = [f.name for f in struct_fields]
114
+
115
+ # Store field names for IDE completion
116
+ cls._field_names_literal = field_names
117
+
118
+ # Create properties for each field to aid IDE completion
119
+ for field_name in field_names:
120
+ if not hasattr(cls, f"_get_{field_name}"):
121
+
122
+ def make_getter(fname):
123
+ def getter(self):
124
+ return getattr(self, fname)
125
+
126
+ return getter
127
+
128
+ setattr(cls, f"_get_{field_name}", make_getter(field_name))
129
+ except Exception:
130
+ # If fields() fails, fallback gracefully
131
+ pass
132
+
133
+ # Remove complex metadata handling - let msgspec handle fields natively
134
+
135
+ @classmethod
136
+ @lru_cache(maxsize=None)
137
+ def _get_field_names(cls) -> tuple[str, ...]:
138
+ """Get all field names as a tuple for type hints."""
139
+ struct_fields = fields(cls)
140
+ return tuple(f.name for f in struct_fields)
141
+
142
+ @classmethod
143
+ @lru_cache(maxsize=None)
144
+ def _get_fields_info(cls) -> Dict[str, Any]:
145
+ """Cached method to get field information."""
146
+ struct_fields = fields(cls)
147
+ result = {}
148
+
149
+ for f in struct_fields:
150
+ field_info = {
151
+ "field": f,
152
+ "type": f.type,
153
+ "required": f.required,
154
+ "default": f.default if not f.required else None,
155
+ }
156
+
157
+ result[f.name] = field_info
158
+
159
+ return result
160
+
161
+ @classmethod
162
+ def model_json_schema(cls) -> dict[str, Any]:
163
+ """Returns the json schema for the object.
164
+
165
+ Uses msgspec's native schema generation when possible for better performance.
166
+ """
167
+ try:
168
+ # Try to use msgspec's native schema generation first
169
+ return schema(cls)
170
+ except Exception:
171
+ # Fallback to manual schema generation
172
+ schema_dict = {
173
+ "type": "object",
174
+ "properties": {},
175
+ "required": [],
176
+ "title": cls.__name__,
177
+ }
178
+
179
+ if cls.__doc__:
180
+ schema_dict["description"] = cls.__doc__.strip()
181
+
182
+ # Get field information from the struct
183
+ fields_info = cls._get_fields_info()
184
+
185
+ for field_name, field_info in fields_info.items():
186
+ field = field_info["field"]
187
+ field_schema = _get_field_schema(field)
188
+ schema_dict["properties"][field_name] = field_schema
189
+
190
+ # Add to required if field has no default
191
+ if field.required:
192
+ schema_dict["required"].append(field_name)
193
+
194
+ # Remove empty required array if no required fields
195
+ if not schema_dict["required"]:
196
+ del schema_dict["required"]
197
+
198
+ return schema_dict
199
+
200
+ def model_dump(
201
+ self,
202
+ *,
203
+ mode: Literal["json", "python"] = "python",
204
+ include: Optional[Union[Set[str], Set[int]]] = None,
205
+ exclude: Optional[Union[Set[str], Set[int]]] = None,
206
+ exclude_none: bool = False,
207
+ exclude_defaults: bool = False,
208
+ exclude_unset: bool = False,
209
+ ) -> Any:
210
+ """Dumps the object into a dictionary, or a json string.
211
+
212
+ Note: exclude_unset is included for compatibility but has no effect
213
+ as msgspec doesn't track unset state.
214
+ """
215
+ # Convert struct to dictionary using msgspec's optimized asdict
216
+ data = asdict(self)
217
+
218
+ # Handle include/exclude filtering
219
+ if include is not None:
220
+ if isinstance(include, set) and all(isinstance(k, str) for k in include):
221
+ data = {k: v for k, v in data.items() if k in include}
222
+ elif isinstance(include, set) and all(isinstance(k, int) for k in include):
223
+ # For integer indices, convert to list of items and filter by index
224
+ items = list(data.items())
225
+ data = dict(items[i] for i in include if 0 <= i < len(items))
226
+
227
+ if exclude is not None:
228
+ if isinstance(exclude, set) and all(isinstance(k, str) for k in exclude):
229
+ data = {k: v for k, v in data.items() if k not in exclude}
230
+ elif isinstance(exclude, set) and all(isinstance(k, int) for k in exclude):
231
+ # For integer indices, convert to list and exclude by index
232
+ items = list(data.items())
233
+ data = dict(items[i] for i in range(len(items)) if i not in exclude)
234
+
235
+ # Handle None exclusion
236
+ if exclude_none:
237
+ data = {k: v for k, v in data.items() if v is not None}
238
+
239
+ # Handle default exclusion
240
+ if exclude_defaults:
241
+ fields_info = self._get_fields_info()
242
+ data = {
243
+ k: v
244
+ for k, v in data.items()
245
+ if k not in fields_info
246
+ or fields_info[k]["required"]
247
+ or v != fields_info[k]["default"]
248
+ }
249
+
250
+ # Return based on mode
251
+ if mode == "python":
252
+ return data
253
+ elif mode == "json":
254
+ return encode(data).decode("utf-8")
255
+ else:
256
+ raise ValueError(f"Invalid mode: {mode}. Must be 'json' or 'python'")
257
+
258
+ def model_dump_json(
259
+ self,
260
+ *,
261
+ indent: Optional[int] = None,
262
+ include: Optional[Union[Set[str], Set[int]]] = None,
263
+ exclude: Optional[Union[Set[str], Set[int]]] = None,
264
+ exclude_none: bool = False,
265
+ exclude_defaults: bool = False,
266
+ ) -> str:
267
+ """Generate a JSON representation of the model."""
268
+ data = self.model_dump(
269
+ mode="python",
270
+ include=include,
271
+ exclude=exclude,
272
+ exclude_none=exclude_none,
273
+ exclude_defaults=exclude_defaults,
274
+ )
275
+ # msgspec's encode is faster than json.dumps
276
+ return encode(data).decode("utf-8")
277
+
278
+ def model_copy(
279
+ self,
280
+ *,
281
+ update: Optional[Dict[str, Any]] = None,
282
+ deep: bool = False,
283
+ exclude: Optional[Union[Set[str], Set[int]]] = None,
284
+ ) -> Self:
285
+ """Create a copy of the struct, optionally updating fields."""
286
+ if update is None:
287
+ update = {}
288
+
289
+ # Get current data as dict using msgspec's optimized asdict
290
+ current_data = asdict(self)
291
+
292
+ # Handle exclude filtering
293
+ if exclude is not None:
294
+ if isinstance(exclude, set) and all(isinstance(k, str) for k in exclude):
295
+ current_data = {
296
+ k: v for k, v in current_data.items() if k not in exclude
297
+ }
298
+ elif isinstance(exclude, set) and all(isinstance(k, int) for k in exclude):
299
+ items = list(current_data.items())
300
+ current_data = dict(
301
+ items[i] for i in range(len(items)) if i not in exclude
302
+ )
303
+
304
+ # Update with new values
305
+ current_data.update(update)
306
+
307
+ # Create new instance
308
+ new_instance = self.__class__(**current_data)
309
+
310
+ if deep:
311
+ # For deep copy, we need to recursively copy nested structures
312
+ return copy.deepcopy(new_instance)
313
+
314
+ return new_instance
315
+
316
+ @classmethod
317
+ def model_validate(cls, obj: Any) -> Self:
318
+ """Validate and create an instance from various input types."""
319
+ if isinstance(obj, cls):
320
+ return obj
321
+ elif isinstance(obj, dict):
322
+ return cls(**obj)
323
+ elif hasattr(obj, "__dict__"):
324
+ return cls(**obj.__dict__)
325
+ else:
326
+ # Try to decode if it's a string/bytes
327
+ try:
328
+ if isinstance(obj, (str, bytes)):
329
+ decoded = decode(obj, type=cls)
330
+ return decoded
331
+ except Exception:
332
+ pass
333
+ raise ValueError(f"Cannot validate {type(obj)} as {cls.__name__}")
334
+
335
+ @classmethod
336
+ def model_validate_json(cls, json_data: Union[str, bytes]) -> Self:
337
+ """Create an instance from JSON string or bytes.
338
+
339
+ Uses msgspec's optimized JSON decoder.
340
+ """
341
+ return decode(json_data, type=cls)
342
+
343
+ @classmethod
344
+ def model_fields(cls) -> Dict[str, Any]:
345
+ """Get information about the struct's fields."""
346
+ return cls._get_fields_info()
347
+
348
+ @classmethod
349
+ def model_load_from_model(
350
+ cls,
351
+ model: Any,
352
+ title: Optional[str] = None,
353
+ description: Optional[str] = None,
354
+ init: bool = False,
355
+ exclude: Optional[Union[Set[str], Set[int]]] = None,
356
+ ) -> Self:
357
+ """Load a model from another model.
358
+
359
+ Args:
360
+ model : The model to load from
361
+ title : An optional title or title override for the model
362
+ description : An optional description for the model
363
+ init : Whether to initialize the model with the field value
364
+ exclude : Fields to exclude from the conversion
365
+ """
366
+ # Extract data from the source model
367
+ if hasattr(model, "model_dump"):
368
+ # It's a pydantic-like model
369
+ source_data = model.model_dump()
370
+ elif hasattr(model, "__dict__"):
371
+ # It's a regular object with attributes
372
+ source_data = model.__dict__.copy()
373
+ elif isinstance(model, dict):
374
+ # It's already a dictionary
375
+ source_data = model.copy()
376
+ elif hasattr(model, "_asdict"):
377
+ # It's a namedtuple
378
+ source_data = model._asdict()
379
+ else:
380
+ # Try to use msgspec's asdict for msgspec structs
381
+ try:
382
+ source_data = asdict(model)
383
+ except Exception:
384
+ # Last resort - try to convert to dict
385
+ try:
386
+ source_data = dict(model)
387
+ except Exception:
388
+ raise ValueError(
389
+ f"Cannot extract data from model of type {type(model)}"
390
+ )
391
+
392
+ # Apply exclusions if specified
393
+ if exclude is not None:
394
+ if isinstance(exclude, set) and all(isinstance(k, str) for k in exclude):
395
+ source_data = {k: v for k, v in source_data.items() if k not in exclude}
396
+ elif isinstance(exclude, set) and all(isinstance(k, int) for k in exclude):
397
+ items = list(source_data.items())
398
+ source_data = dict(
399
+ items[i] for i in range(len(items)) if i not in exclude
400
+ )
401
+
402
+ # Get the fields of the target class to filter compatible fields
403
+ try:
404
+ target_fields = cls._get_field_names()
405
+ # Only include fields that exist in the target model
406
+ filtered_data = {k: v for k, v in source_data.items() if k in target_fields}
407
+ except Exception:
408
+ # If we can't get field names, use all data
409
+ filtered_data = source_data
410
+
411
+ if init:
412
+ # Create and return an instance
413
+ return cls(**filtered_data)
414
+ else:
415
+ # Return the class type - this doesn't make much sense for the method signature
416
+ # but following the parameter description, we'll return an uninitialized version
417
+ # In practice, this would typically return the class itself or raise an error
418
+ # For now, let's create an instance anyway since that's most useful
419
+ return cls(**filtered_data)
420
+
421
+ @classmethod
422
+ def model_field_to_model(
423
+ cls,
424
+ fields: str,
425
+ schema: Literal[
426
+ "based",
427
+ "dataclass",
428
+ "pydantic",
429
+ "msgspec",
430
+ "typeddict",
431
+ "namedtuple",
432
+ "attrs",
433
+ "dict",
434
+ ] = "based",
435
+ # Simple Override Params To Edit The Final Model
436
+ # This method always goes field(s) -> model not to field
437
+ title: Optional[str] = None,
438
+ description: Optional[str] = None,
439
+ field_name: str = "value",
440
+ field_description: Optional[str] = None,
441
+ field_examples: Optional[List[Any]] = None,
442
+ init: bool = False,
443
+ ) -> Any:
444
+ """Convert a single field to a new model of any
445
+ type.
446
+
447
+ Args:
448
+ fields: The field to be converted into the model
449
+ schema: The target schema format to convert to (Defaults to a basedmodel)
450
+ title : An optional title or title override for the model (uses the field name if not provided)
451
+ description : An optional description for the model (uses the field description if not provided)
452
+ field_name : The name of the field within this new model representing the target field (defaults to "value")
453
+ field_description : An optional description for the field within this new model (defaults to None)
454
+ field_examples : An optional list of examples for the field within this new model (defaults to None)
455
+ init : Whether to initialize the model with the field value (defaults to False)
456
+ """
457
+ # Get field information from the class
458
+ fields_info = cls._get_fields_info()
459
+
460
+ if fields not in fields_info:
461
+ raise ValueError(f"Field '{fields}' not found in {cls.__name__}")
462
+
463
+ field_info = fields_info[fields]
464
+ field_type = field_info["type"]
465
+
466
+ # Handle default values properly, including default_factory
467
+ if not field_info["required"]:
468
+ field_default = field_info["default"]
469
+ # Check for default_factory in the msgspec field
470
+ msgspec_field = field_info["field"]
471
+ if (
472
+ hasattr(msgspec_field, "default_factory")
473
+ and msgspec_field.default_factory is not msgspec.UNSET
474
+ and msgspec_field.default_factory is not msgspec.NODEFAULT
475
+ ):
476
+ # It has a default_factory, call it to get the actual default
477
+ try:
478
+ field_default = msgspec_field.default_factory()
479
+ except Exception:
480
+ # If calling fails, use UNSET
481
+ field_default = msgspec.UNSET
482
+ # If field_default is NODEFAULT but no default_factory, keep as UNSET
483
+ elif field_default is msgspec.NODEFAULT:
484
+ field_default = msgspec.UNSET
485
+ else:
486
+ field_default = msgspec.UNSET
487
+
488
+ # Use provided title or default to field name
489
+ model_title = title or fields.title()
490
+ model_description = description or f"Model wrapping field '{fields}'"
491
+
492
+ if schema == "based":
493
+ from .fields import basedfield
494
+
495
+ # Create annotations for the dynamic class
496
+ annotations = {field_name: field_type}
497
+
498
+ # Create field definition
499
+ class_dict = {"__annotations__": annotations}
500
+
501
+ # Add default if available
502
+ if field_default is not msgspec.UNSET:
503
+ class_dict[field_name] = basedfield(
504
+ default=field_default,
505
+ description=field_description,
506
+ examples=field_examples,
507
+ )
508
+ elif field_description or field_examples:
509
+ class_dict[field_name] = basedfield(
510
+ description=field_description, examples=field_examples
511
+ )
512
+
513
+ # Create the dynamic class
514
+ DynamicModel = type(model_title.replace(" ", ""), (BasedModel,), class_dict)
515
+
516
+ if init and field_default is not msgspec.UNSET:
517
+ return DynamicModel(**{field_name: field_default})
518
+ elif init:
519
+ # Need a value to initialize with
520
+ raise ValueError("Cannot initialize model without a default value")
521
+ else:
522
+ return DynamicModel
523
+
524
+ elif schema == "dataclass":
525
+ from dataclasses import make_dataclass, field as dc_field
526
+
527
+ if field_default is not msgspec.UNSET:
528
+ fields_list = [
529
+ (field_name, field_type, dc_field(default=field_default))
530
+ ]
531
+ else:
532
+ fields_list = [(field_name, field_type)]
533
+
534
+ DynamicDataclass = make_dataclass(model_title.replace(" ", ""), fields_list)
535
+
536
+ if init and field_default is not msgspec.UNSET:
537
+ return DynamicDataclass(**{field_name: field_default})
538
+ elif init:
539
+ raise ValueError("Cannot initialize dataclass without a default value")
540
+ else:
541
+ return DynamicDataclass
542
+
543
+ elif schema == "pydantic":
544
+ from pydantic import BaseModel, create_model
545
+
546
+ pydantic_fields = {}
547
+ if field_default is not msgspec.UNSET:
548
+ pydantic_fields[field_name] = (field_type, field_default)
549
+ else:
550
+ pydantic_fields[field_name] = (field_type, ...)
551
+
552
+ PydanticModel = create_model(
553
+ model_title.replace(" ", ""), **pydantic_fields
554
+ )
555
+
556
+ if init and field_default is not msgspec.UNSET:
557
+ return PydanticModel(**{field_name: field_default})
558
+ elif init:
559
+ raise ValueError(
560
+ "Cannot initialize pydantic model without a default value"
561
+ )
562
+ else:
563
+ return PydanticModel
564
+
565
+ elif schema == "msgspec":
566
+ # Create a msgspec Struct dynamically
567
+ struct_fields = {field_name: field_type}
568
+ if field_default is not msgspec.UNSET:
569
+ struct_fields[field_name] = msgspec_field(default=field_default)
570
+
571
+ DynamicStruct = type(
572
+ model_title.replace(" ", ""),
573
+ (Struct,),
574
+ {"__annotations__": {field_name: field_type}},
575
+ )
576
+
577
+ if init and field_default is not msgspec.UNSET:
578
+ return DynamicStruct(**{field_name: field_default})
579
+ elif init:
580
+ raise ValueError(
581
+ "Cannot initialize msgspec struct without a default value"
582
+ )
583
+ else:
584
+ return DynamicStruct
585
+
586
+ elif schema == "typeddict":
587
+ from typing import TypedDict
588
+
589
+ # TypedDict can't be created dynamically in the same way
590
+ # Return a dictionary with type information
591
+ if init and field_default is not msgspec.UNSET:
592
+ return {field_name: field_default}
593
+ elif init:
594
+ raise ValueError("Cannot initialize TypedDict without a default value")
595
+ else:
596
+ # Return a TypedDict class (though this is limited)
597
+ return TypedDict(model_title.replace(" ", ""), {field_name: field_type})
598
+
599
+ elif schema == "namedtuple":
600
+ from collections import namedtuple
601
+
602
+ DynamicNamedTuple = namedtuple(model_title.replace(" ", ""), [field_name])
603
+
604
+ if init and field_default is not msgspec.UNSET:
605
+ return DynamicNamedTuple(**{field_name: field_default})
606
+ elif init:
607
+ raise ValueError("Cannot initialize namedtuple without a default value")
608
+ else:
609
+ return DynamicNamedTuple
610
+
611
+ elif schema == "attrs":
612
+ try:
613
+ import attrs
614
+
615
+ if field_default is not msgspec.UNSET:
616
+ field_attr = attrs.field(default=field_default)
617
+ else:
618
+ field_attr = attrs.field()
619
+
620
+ @attrs.define
621
+ class DynamicAttrs:
622
+ pass
623
+
624
+ # Set the field dynamically
625
+ setattr(DynamicAttrs, field_name, field_attr)
626
+ DynamicAttrs.__annotations__ = {field_name: field_type}
627
+
628
+ if init and field_default is not msgspec.UNSET:
629
+ return DynamicAttrs(**{field_name: field_default})
630
+ elif init:
631
+ raise ValueError(
632
+ "Cannot initialize attrs class without a default value"
633
+ )
634
+ else:
635
+ return DynamicAttrs
636
+
637
+ except ImportError:
638
+ raise ImportError("attrs library is required for attrs conversion")
639
+
640
+ elif schema == "dict":
641
+ if init and field_default is not msgspec.UNSET:
642
+ return {field_name: field_default}
643
+ elif init:
644
+ raise ValueError("Cannot initialize dict without a default value")
645
+ else:
646
+ return {field_name: field_type}
647
+
648
+ else:
649
+ raise ValueError(f"Unsupported schema format: {schema}")
650
+
651
+ def model_convert(
652
+ self,
653
+ schema: Literal[
654
+ "dataclass",
655
+ "pydantic",
656
+ "msgspec",
657
+ "typeddict",
658
+ "namedtuple",
659
+ "attrs",
660
+ "dict",
661
+ ],
662
+ exclude: Optional[Union[Set[str], Set[int]]] = None,
663
+ ) -> Any:
664
+ """Convert the model to different schema formats using adaptix.
665
+
666
+ Args:
667
+ schema: The target schema format to convert to
668
+ exclude: Fields to exclude from the conversion
669
+
670
+ Returns:
671
+ The converted model in the specified format
672
+ """
673
+ # Get current model data
674
+ current_data = asdict(self)
675
+
676
+ # Apply exclusions if specified
677
+ if exclude is not None:
678
+ if isinstance(exclude, set) and all(isinstance(k, str) for k in exclude):
679
+ current_data = {
680
+ k: v for k, v in current_data.items() if k not in exclude
681
+ }
682
+ elif isinstance(exclude, set) and all(isinstance(k, int) for k in exclude):
683
+ items = list(current_data.items())
684
+ current_data = dict(
685
+ items[i] for i in range(len(items)) if i not in exclude
686
+ )
687
+
688
+ if schema == "dataclass":
689
+ # Create a dynamic dataclass using make_dataclass
690
+ from dataclasses import make_dataclass, field
691
+
692
+ field_info = self._get_fields_info()
693
+ fields_list = []
694
+
695
+ for field_name, info in field_info.items():
696
+ if field_name not in current_data:
697
+ continue
698
+ field_type = info["type"]
699
+ if info["required"]:
700
+ fields_list.append((field_name, field_type))
701
+ else:
702
+ fields_list.append(
703
+ (field_name, field_type, field(default=info["default"]))
704
+ )
705
+
706
+ DynamicDataclass = make_dataclass(
707
+ f"Dynamic{self.__class__.__name__}", fields_list
708
+ )
709
+
710
+ return DynamicDataclass(**current_data)
711
+
712
+ elif schema == "pydantic":
713
+ from pydantic import BaseModel, create_model
714
+
715
+ field_info = self._get_fields_info()
716
+ pydantic_fields = {}
717
+
718
+ for field_name, info in field_info.items():
719
+ if field_name not in current_data:
720
+ continue
721
+ field_type = info["type"]
722
+ if info["required"]:
723
+ pydantic_fields[field_name] = (field_type, ...)
724
+ else:
725
+ pydantic_fields[field_name] = (field_type, info["default"])
726
+
727
+ PydanticModel = create_model(
728
+ f"Pydantic{self.__class__.__name__}", **pydantic_fields
729
+ )
730
+ return PydanticModel(**current_data)
731
+
732
+ elif schema == "msgspec":
733
+ # Return as msgspec Struct (already is one)
734
+ return self.__class__(**current_data)
735
+
736
+ elif schema == "typeddict":
737
+ # TypedDict doesn't have constructor, just return the dict with type info
738
+ return current_data
739
+
740
+ elif schema == "namedtuple":
741
+ from collections import namedtuple
742
+
743
+ field_names = list(current_data.keys())
744
+ DynamicNamedTuple = namedtuple(
745
+ f"Dynamic{self.__class__.__name__}", field_names
746
+ )
747
+ return DynamicNamedTuple(**current_data)
748
+
749
+ elif schema == "attrs":
750
+ try:
751
+ import attrs
752
+
753
+ field_info = self._get_fields_info()
754
+ attrs_fields = []
755
+
756
+ for field_name, info in field_info.items():
757
+ if field_name not in current_data:
758
+ continue
759
+ if info["required"]:
760
+ attrs_fields.append(attrs.field())
761
+ else:
762
+ attrs_fields.append(attrs.field(default=info["default"]))
763
+
764
+ @attrs.define
765
+ class DynamicAttrs:
766
+ pass
767
+
768
+ # Set fields dynamically
769
+ for i, field_name in enumerate(current_data.keys()):
770
+ setattr(DynamicAttrs, field_name, attrs_fields[i])
771
+
772
+ return DynamicAttrs(**current_data)
773
+
774
+ except ImportError:
775
+ raise ImportError("attrs library is required for attrs conversion")
776
+
777
+ elif schema == "dict":
778
+ return current_data
779
+
780
+ else:
781
+ raise ValueError(f"Unsupported schema format: {schema}")
782
+
783
+ @classmethod
784
+ def convert_from_data(
785
+ cls,
786
+ data: Any,
787
+ schema: Literal[
788
+ "dataclass",
789
+ "pydantic",
790
+ "msgspec",
791
+ "typeddict",
792
+ "namedtuple",
793
+ "attrs",
794
+ "dict",
795
+ ],
796
+ exclude: Optional[Union[Set[str], Set[int]]] = None,
797
+ ) -> Any:
798
+ """Class method to convert data to different schema formats.
799
+
800
+ Args:
801
+ data: Input data to convert (dict, object, etc.)
802
+ schema: The target schema format to convert to
803
+ exclude: Fields to exclude from the conversion
804
+
805
+ Returns:
806
+ The converted model in the specified format
807
+ """
808
+ # First create an instance from the data
809
+ if isinstance(data, dict):
810
+ instance = cls(**data)
811
+ elif hasattr(data, "__dict__"):
812
+ instance = cls(**data.__dict__)
813
+ else:
814
+ instance = cls.model_validate(data)
815
+
816
+ # Then use the instance method to convert
817
+ return instance.model_convert(schema, exclude)
818
+
819
+ def model_to_pydantic(self):
820
+ """Converts the `BasedModel` to a `pydantic.BaseModel`."""
821
+ from pydantic import BaseModel, create_model
822
+
823
+ # Get the field information from the current instance
824
+ fields_info = self._get_fields_info()
825
+
826
+ # Create a dictionary for pydantic model fields
827
+ pydantic_fields = {}
828
+ for field_name, field_info in fields_info.items():
829
+ field_type = field_info["type"]
830
+ default = field_info["default"]
831
+ required = field_info["required"]
832
+
833
+ if required:
834
+ pydantic_fields[field_name] = (field_type, ...)
835
+ else:
836
+ pydantic_fields[field_name] = (field_type, default)
837
+
838
+ # Create a dynamic pydantic model class
839
+ PydanticModel = create_model(
840
+ f"Pydantic{self.__class__.__name__}", **pydantic_fields
841
+ )
842
+
843
+ # Create an instance with the current data
844
+ current_data = asdict(self)
845
+ return PydanticModel(**current_data)
846
+
847
+ def __str__(self) -> str:
848
+ """String representation of the struct."""
849
+ return f"{self.__class__.__name__}({', '.join(f'{k}={repr(v)}' for k, v in asdict(self).items())})"
850
+
851
+ def __repr__(self) -> str:
852
+ """Detailed string representation of the struct."""
853
+ return self.__str__()
854
+
855
+ # Dictionary access methods for compatibility
856
+ def __getitem__(self, key: str) -> Any:
857
+ """Get an item from the struct with IDE field completion support."""
858
+ if not hasattr(self, key):
859
+ raise KeyError(f"'{key}' not found in {self.__class__.__name__}")
860
+ return getattr(self, key)
861
+
862
+ def get_field(self, field_name: str) -> Any:
863
+ """Get a field value with better IDE completion. Use: model.get_field('field_name')"""
864
+ if not hasattr(self, field_name):
865
+ raise KeyError(f"'{field_name}' not found in {self.__class__.__name__}")
866
+ return getattr(self, field_name)
867
+
868
+ @property
869
+ def field_keys(self) -> tuple[str, ...]:
870
+ """Get all available field names as a tuple for IDE completion."""
871
+ return tuple(self.__struct_fields__)
872
+
873
+ def fields(self):
874
+ """Returns an accessor object with all fields for IDE completion."""
875
+
876
+ class FieldAccessor:
877
+ def __init__(self, instance):
878
+ self._instance = instance
879
+ # Dynamically set all field names as properties for IDE completion
880
+ struct_fields = list(instance.__struct_fields__)
881
+ self.__dict__.update(
882
+ {name: getattr(instance, name) for name in struct_fields}
883
+ )
884
+
885
+ def __getitem__(self, field_key: str) -> Any:
886
+ if not hasattr(self._instance, field_key):
887
+ raise KeyError(
888
+ f"'{field_key}' not found in {self._instance.__class__.__name__}"
889
+ )
890
+ return getattr(self._instance, field_key)
891
+
892
+ def __dir__(self):
893
+ return list(self._instance.__struct_fields__)
894
+
895
+ def keys(self):
896
+ """Get all field names."""
897
+ return list(self._instance.__struct_fields__)
898
+
899
+ def values(self):
900
+ """Get all field values."""
901
+ return [
902
+ getattr(self._instance, name)
903
+ for name in self._instance.__struct_fields__
904
+ ]
905
+
906
+ def items(self):
907
+ """Get all field name-value pairs."""
908
+ return [
909
+ (name, getattr(self._instance, name))
910
+ for name in self._instance.__struct_fields__
911
+ ]
912
+
913
+ return FieldAccessor(self)
914
+
915
+ def __setitem__(self, key: str, value: Any) -> None:
916
+ """Set an item in the struct."""
917
+ if key not in self.__struct_fields__:
918
+ raise KeyError(
919
+ f"'{key}' is not a valid field for {self.__class__.__name__}"
920
+ )
921
+ setattr(self, key, value)
922
+
923
+ def __delitem__(self, key: str) -> None:
924
+ """Delete an item from the struct.
925
+
926
+ Note: This will raise an error as struct fields cannot be deleted.
927
+ """
928
+ raise TypeError(f"Cannot delete field '{key}' from immutable struct")
929
+
930
+ def __contains__(self, key: str) -> bool:
931
+ """Check if the struct contains a field."""
932
+ return key in self.__struct_fields__
933
+
934
+ def __iter__(self):
935
+ """Iterate over field names."""
936
+ return iter(self.__struct_fields__)
937
+
938
+ def __dir__(self) -> list[str]:
939
+ """Allows for IDE autocompletion of the model's fields
940
+ when accessing through the dictionary interface."""
941
+ # Include both parent attributes and field names
942
+ base_attrs = super().__dir__()
943
+ field_names = list(self.__struct_fields__)
944
+
945
+ # Add some useful methods and properties
946
+ additional_attrs = [
947
+ "model_dump",
948
+ "model_dump_json",
949
+ "model_copy",
950
+ "model_validate",
951
+ "model_validate_json",
952
+ "model_fields",
953
+ "model_json_schema",
954
+ "model_to_pydantic",
955
+ "model_convert",
956
+ "fields",
957
+ ]
958
+
959
+ return list(set(base_attrs + field_names + additional_attrs))
960
+
961
+ # Support for __post_init__ if needed (from msgspec 0.18.0+)
962
+ def __post_init__(self) -> None:
963
+ """Called after struct initialization.
964
+
965
+ Override this method in subclasses to add post-initialization logic.
966
+ This is called automatically by msgspec after the struct is created.
967
+ """
968
+ pass