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
@@ -0,0 +1,534 @@
1
+ """hammad.logging.logger"""
2
+
3
+ import logging as _logging
4
+ import inspect
5
+ from dataclasses import dataclass, field
6
+ from typing import (
7
+ Literal,
8
+ TypeAlias,
9
+ NamedTuple,
10
+ ParamSpec,
11
+ TypeVar,
12
+ Dict,
13
+ Optional,
14
+ Any,
15
+ Union,
16
+ )
17
+ from typing_extensions import TypedDict
18
+
19
+ from rich import get_console as get_rich_console
20
+ from rich.logging import RichHandler
21
+
22
+ from ..cli.styles.types import (
23
+ CLIStyleType,
24
+ )
25
+ from ..cli.styles.settings import CLIStyleRenderableSettings, CLIStyleBackgroundSettings
26
+
27
+ __all__ = (
28
+ "Logger",
29
+ "create_logger",
30
+ "create_logger_level",
31
+ )
32
+
33
+
34
+ # -----------------------------------------------------------------------------
35
+ # Types
36
+ # -----------------------------------------------------------------------------
37
+
38
+
39
+ LoggerLevelName: TypeAlias = Literal["debug", "info", "warning", "error", "critical"]
40
+ """Literal type helper for logging levels."""
41
+
42
+
43
+ _P = ParamSpec("_P")
44
+ _R = TypeVar("_R")
45
+
46
+
47
+ class LoggerLevelSettings(TypedDict, total=False):
48
+ """Configuration dictionary for the display style of a
49
+ single logging level."""
50
+
51
+ title: CLIStyleType | CLIStyleRenderableSettings
52
+ """Either a string tag or style settings for the title output
53
+ of the messages of this level. This includes module name
54
+ and level name."""
55
+
56
+ message: CLIStyleType | CLIStyleRenderableSettings
57
+ """Either a string tag or style settings for the message output
58
+ of the messages of this level. This includes the message itself."""
59
+
60
+ background: CLIStyleType | CLIStyleBackgroundSettings
61
+ """Either a string tag or style settings for the background output
62
+ of the messages of this level. This includes the message itself."""
63
+
64
+
65
+ # -----------------------------------------------------------------------------
66
+ # Default Level Styles
67
+ # -----------------------------------------------------------------------------
68
+
69
+ DEFAULT_LEVEL_STYLES: Dict[str, LoggerLevelSettings] = {
70
+ "critical": {
71
+ "message": "red bold",
72
+ },
73
+ "error": {
74
+ "message": "red italic",
75
+ },
76
+ "warning": {
77
+ "message": "yellow italic",
78
+ },
79
+ "info": {
80
+ "message": "white",
81
+ },
82
+ "debug": {
83
+ "message": "white italic dim",
84
+ },
85
+ }
86
+
87
+
88
+ # -----------------------------------------------------------------------------
89
+ # Logging Filter
90
+ # -----------------------------------------------------------------------------
91
+
92
+
93
+ class RichLoggerFilter(_logging.Filter):
94
+ """Filter for applying rich styling to log messages based on level."""
95
+
96
+ def __init__(self, level_styles: Dict[str, LoggerLevelSettings]):
97
+ super().__init__()
98
+ self.level_styles = level_styles
99
+
100
+ def filter(self, record: _logging.LogRecord) -> bool:
101
+ # Get the level name
102
+ level_name = record.levelname.lower()
103
+
104
+ # Check if we have custom styling for this level
105
+ if level_name in self.level_styles:
106
+ style_config = self.level_styles[level_name]
107
+
108
+ # We'll use a special attribute to store style config
109
+ # The formatter/handler will use this to apply styling
110
+ record._hammad_style_config = style_config
111
+
112
+ return True
113
+
114
+
115
+ # -----------------------------------------------------------------------------
116
+ # Custom Rich Formatter
117
+ # -----------------------------------------------------------------------------
118
+
119
+
120
+ class RichLoggerFormatter(_logging.Formatter):
121
+ """Custom formatter that applies rich styling."""
122
+
123
+ def __init__(self, *args, **kwargs):
124
+ super().__init__(*args, **kwargs)
125
+ self.console = get_rich_console()
126
+
127
+ def formatMessage(self, record: _logging.LogRecord) -> str:
128
+ """Override formatMessage to apply styling to different parts."""
129
+ # Check if we have style configuration
130
+ if hasattr(record, "_hammad_style_config"):
131
+ style_config = record._hammad_style_config
132
+
133
+ # Handle title styling (logger name)
134
+ title_style = style_config.get("title", None)
135
+ if title_style:
136
+ if isinstance(title_style, str):
137
+ # It's a color/style string tag
138
+ record.name = f"[{title_style}]{record.name}[/{title_style}]"
139
+ elif isinstance(title_style, dict):
140
+ # It's a CLIStyleRenderableSettings dict
141
+ style_str = self._build_renderable_style_string(title_style)
142
+ if style_str:
143
+ record.name = f"[{style_str}]{record.name}[/{style_str}]"
144
+
145
+ # Handle message styling
146
+ message_style = style_config.get("message", None)
147
+ if message_style:
148
+ if isinstance(message_style, str):
149
+ # It's a color/style string tag
150
+ record.message = (
151
+ f"[{message_style}]{record.getMessage()}[/{message_style}]"
152
+ )
153
+ elif isinstance(message_style, dict):
154
+ # It's a CLIStyleRenderableSettings dict
155
+ style_str = self._build_renderable_style_string(message_style)
156
+ if style_str:
157
+ record.message = (
158
+ f"[{style_str}]{record.getMessage()}[/{style_str}]"
159
+ )
160
+ else:
161
+ record.message = record.getMessage()
162
+ else:
163
+ record.message = record.getMessage()
164
+ else:
165
+ record.message = record.getMessage()
166
+
167
+ # Now format with the styled values
168
+ return self._style._fmt.format(**record.__dict__)
169
+
170
+ def _build_renderable_style_string(self, style_dict: dict) -> str:
171
+ """Build a rich markup style string from a CLIStyleRenderableSettings dictionary."""
172
+ style_parts = []
173
+
174
+ # Handle all the style attributes from CLIStyleRenderableSettings
175
+ for attr in [
176
+ "bold",
177
+ "italic",
178
+ "dim",
179
+ "underline",
180
+ "strike",
181
+ "blink",
182
+ "blink2",
183
+ "reverse",
184
+ "conceal",
185
+ "underline2",
186
+ "frame",
187
+ "encircle",
188
+ "overline",
189
+ ]:
190
+ if style_dict.get(attr):
191
+ style_parts.append(attr)
192
+
193
+ return " ".join(style_parts) if style_parts else ""
194
+
195
+
196
+ # -----------------------------------------------------------------------------
197
+ # Logger
198
+ # -----------------------------------------------------------------------------
199
+
200
+
201
+ @dataclass
202
+ class Logger:
203
+ """Flexible logger with rich styling and custom level support."""
204
+
205
+ _logger: _logging.Logger = field(init=False)
206
+ """The underlying logging.Logger instance."""
207
+
208
+ _level_styles: Dict[str, LoggerLevelSettings] = field(init=False)
209
+ """Custom level styles."""
210
+
211
+ _custom_levels: Dict[str, int] = field(init=False)
212
+ """Custom logging levels."""
213
+
214
+ _user_level: str = field(init=False)
215
+ """User-specified logging level."""
216
+
217
+ def __init__(
218
+ self,
219
+ name: Optional[str] = None,
220
+ level: Optional[Union[str, int]] = None,
221
+ rich: bool = True,
222
+ display_all: bool = False,
223
+ level_styles: Optional[Dict[str, LoggerLevelSettings]] = None,
224
+ ) -> None:
225
+ """
226
+ Initialize a new Logger instance.
227
+
228
+ Args:
229
+ name: Name for the logger. If None, defaults to "hammad"
230
+ level: Logging level. If None, defaults to "debug" if display_all else "warning"
231
+ rich: Whether to use rich formatting for output
232
+ display_all: If True, sets effective level to debug to show all messages
233
+ level_styles: Custom level styles to override defaults
234
+ """
235
+ logger_name = name or "hammad"
236
+
237
+ # Initialize custom levels dict
238
+ self._custom_levels = {}
239
+
240
+ # Initialize level styles with defaults
241
+ self._level_styles = DEFAULT_LEVEL_STYLES.copy()
242
+ if level_styles:
243
+ self._level_styles.update(level_styles)
244
+
245
+ # Handle integer levels by converting to string names
246
+ if isinstance(level, int):
247
+ # Map standard logging levels to their names
248
+ int_to_name = {
249
+ _logging.DEBUG: "debug",
250
+ _logging.INFO: "info",
251
+ _logging.WARNING: "warning",
252
+ _logging.ERROR: "error",
253
+ _logging.CRITICAL: "critical",
254
+ }
255
+ level = int_to_name.get(level, "warning")
256
+
257
+ self._user_level = level or "warning"
258
+
259
+ if display_all:
260
+ effective_level = "debug"
261
+ else:
262
+ effective_level = self._user_level
263
+
264
+ # Standard level mapping
265
+ level_map = {
266
+ "debug": _logging.DEBUG,
267
+ "info": _logging.INFO,
268
+ "warning": _logging.WARNING,
269
+ "error": _logging.ERROR,
270
+ "critical": _logging.CRITICAL,
271
+ }
272
+
273
+ # Check if it's a custom level
274
+ if effective_level.lower() in self._custom_levels:
275
+ log_level = self._custom_levels[effective_level.lower()]
276
+ else:
277
+ log_level = level_map.get(effective_level.lower(), _logging.WARNING)
278
+
279
+ # Create logger
280
+ self._logger = _logging.getLogger(logger_name)
281
+
282
+ # Clear any existing handlers
283
+ if self._logger.hasHandlers():
284
+ self._logger.handlers.clear()
285
+
286
+ # Setup handler based on rich preference
287
+ if rich:
288
+ self._setup_rich_handler(log_level)
289
+ else:
290
+ self._setup_standard_handler(log_level)
291
+
292
+ self._logger.setLevel(log_level)
293
+ self._logger.propagate = False
294
+
295
+ def _setup_rich_handler(self, log_level: int) -> None:
296
+ """Setup rich handler for the logger."""
297
+ console = get_rich_console()
298
+
299
+ handler = RichHandler(
300
+ level=log_level,
301
+ console=console,
302
+ rich_tracebacks=True,
303
+ show_time=False,
304
+ show_path=False,
305
+ markup=True,
306
+ )
307
+
308
+ formatter = RichLoggerFormatter(
309
+ "| [bold]✼ {name}[/bold] - {message}", style="{"
310
+ )
311
+ handler.setFormatter(formatter)
312
+
313
+ # Add our custom filter
314
+ handler.addFilter(RichLoggerFilter(self._level_styles))
315
+
316
+ self._logger.addHandler(handler)
317
+
318
+ def _setup_standard_handler(self, log_level: int) -> None:
319
+ """Setup standard handler for the logger."""
320
+ handler = _logging.StreamHandler()
321
+ formatter = _logging.Formatter("✼ {name} - {levelname} - {message}", style="{")
322
+ handler.setFormatter(formatter)
323
+ handler.setLevel(log_level)
324
+
325
+ self._logger.addHandler(handler)
326
+
327
+ def add_level(
328
+ self, name: str, value: int, style: Optional[LoggerLevelSettings] = None
329
+ ) -> None:
330
+ """
331
+ Add a custom logging level.
332
+
333
+ Args:
334
+ name: Name of the custom level
335
+ value: Numeric value for the level (should be unique)
336
+ style: Optional style settings for the level
337
+ """
338
+ # Add to Python's logging module
339
+ _logging.addLevelName(value, name.upper())
340
+
341
+ # Store in our custom levels
342
+ self._custom_levels[name.lower()] = value
343
+
344
+ # Add style if provided
345
+ if style:
346
+ self._level_styles[name.lower()] = style
347
+
348
+ # Update filters if using rich handler
349
+ for handler in self._logger.handlers:
350
+ if isinstance(handler, RichHandler):
351
+ # Remove old filter and add new one with updated styles
352
+ for f in handler.filters[:]:
353
+ if isinstance(f, RichLoggerFilter):
354
+ handler.removeFilter(f)
355
+ handler.addFilter(RichLoggerFilter(self._level_styles))
356
+
357
+ @property
358
+ def level(self) -> str:
359
+ """Get the current logging level."""
360
+ return self._user_level
361
+
362
+ @level.setter
363
+ def level(self, value: str) -> None:
364
+ """Set the logging level."""
365
+ self._user_level = value
366
+
367
+ # Standard level mapping
368
+ level_map = {
369
+ "debug": _logging.DEBUG,
370
+ "info": _logging.INFO,
371
+ "warning": _logging.WARNING,
372
+ "error": _logging.ERROR,
373
+ "critical": _logging.CRITICAL,
374
+ }
375
+
376
+ # Check custom levels
377
+ if value.lower() in self._custom_levels:
378
+ log_level = self._custom_levels[value.lower()]
379
+ else:
380
+ log_level = level_map.get(value.lower(), _logging.WARNING)
381
+
382
+ # Update logger level
383
+ self._logger.setLevel(log_level)
384
+
385
+ # Update handler levels
386
+ for handler in self._logger.handlers:
387
+ handler.setLevel(log_level)
388
+
389
+ # Convenience methods for standard logging levels
390
+ def debug(self, message: str, *args: Any, **kwargs: Any) -> None:
391
+ """Log a debug message."""
392
+ self._logger.debug(message, *args, **kwargs)
393
+
394
+ def info(self, message: str, *args: Any, **kwargs: Any) -> None:
395
+ """Log an info message."""
396
+ self._logger.info(message, *args, **kwargs)
397
+
398
+ def warning(self, message: str, *args: Any, **kwargs: Any) -> None:
399
+ """Log a warning message."""
400
+ self._logger.warning(message, *args, **kwargs)
401
+
402
+ def error(self, message: str, *args: Any, **kwargs: Any) -> None:
403
+ """Log an error message."""
404
+ self._logger.error(message, *args, **kwargs)
405
+
406
+ def critical(self, message: str, *args: Any, **kwargs: Any) -> None:
407
+ """Log a critical message."""
408
+ self._logger.critical(message, *args, **kwargs)
409
+
410
+ def log(
411
+ self, level: Union[str, int], message: str, *args: Any, **kwargs: Any
412
+ ) -> None:
413
+ """
414
+ Log a message at the specified level.
415
+
416
+ Args:
417
+ level: The level to log at (can be standard or custom)
418
+ message: The message to log
419
+ *args: Additional positional arguments for the logger
420
+ **kwargs: Additional keyword arguments for the logger
421
+ """
422
+ # Standard level mapping
423
+ level_map = {
424
+ "debug": _logging.DEBUG,
425
+ "info": _logging.INFO,
426
+ "warning": _logging.WARNING,
427
+ "error": _logging.ERROR,
428
+ "critical": _logging.CRITICAL,
429
+ }
430
+
431
+ # Handle integer levels
432
+ if isinstance(level, int):
433
+ # Use the integer level directly
434
+ log_level = level
435
+ else:
436
+ # Check custom levels first
437
+ if level.lower() in self._custom_levels:
438
+ log_level = self._custom_levels[level.lower()]
439
+ else:
440
+ log_level = level_map.get(level.lower(), _logging.WARNING)
441
+
442
+ self._logger.log(log_level, message, *args, **kwargs)
443
+
444
+ @property
445
+ def name(self) -> str:
446
+ """Get the logger name."""
447
+ return self._logger.name
448
+
449
+ @property
450
+ def handlers(self) -> list[_logging.Handler]:
451
+ """Get the logger handlers."""
452
+ return self._logger.handlers
453
+
454
+ def get_logger(self) -> _logging.Logger:
455
+ """Get the underlying logging.Logger instance."""
456
+ return self._logger
457
+
458
+
459
+ # -----------------------------------------------------------------------------
460
+ # Factory
461
+ # -----------------------------------------------------------------------------
462
+
463
+
464
+ def create_logger_level(
465
+ name: str,
466
+ level: int,
467
+ color: Optional[str] = None,
468
+ style: Optional[str] = None,
469
+ ) -> None:
470
+ """
471
+ Create a custom logging level.
472
+
473
+ Args:
474
+ name: The name of the logging level (e.g., "TRACE", "SUCCESS")
475
+ level: The numeric level value (should be between existing levels)
476
+ color: Optional color for rich formatting (e.g., "green", "blue")
477
+ style: Optional style for rich formatting (e.g., "bold", "italic")
478
+ """
479
+ # Convert name to uppercase for consistency
480
+ level_name = name.upper()
481
+
482
+ # Add the level to the logging module
483
+ _logging.addLevelName(level, level_name)
484
+
485
+ # Create a method on the Logger class for this level
486
+ def log_method(self, message, *args, **kwargs):
487
+ if self.isEnabledFor(level):
488
+ self._log(level, message, args, **kwargs)
489
+
490
+ # Add the method to the standard logging.Logger class
491
+ setattr(_logging.Logger, name.lower(), log_method)
492
+
493
+ # Store level info for potential rich formatting
494
+ if hasattr(_logging, "_custom_level_info"):
495
+ _logging._custom_level_info[level] = {
496
+ "name": level_name,
497
+ "color": color,
498
+ "style": style,
499
+ }
500
+ else:
501
+ _logging._custom_level_info = {
502
+ level: {"name": level_name, "color": color, "style": style}
503
+ }
504
+
505
+
506
+ def create_logger(
507
+ name: Optional[str] = None,
508
+ level: Optional[Union[str, int]] = None,
509
+ rich: bool = True,
510
+ display_all: bool = False,
511
+ levels: Optional[Dict[LoggerLevelName, LoggerLevelSettings]] = None,
512
+ ) -> Logger:
513
+ """
514
+ Get a logger instance.
515
+
516
+ Args:
517
+ name: Name for the logger. If None, uses caller's function name
518
+ level: Logging level. If None, defaults to "debug" if display_all else "warning"
519
+ rich: Whether to use rich formatting for output
520
+ display_all: If True, sets effective level to debug to show all messages
521
+ levels: Custom level styles to override defaults. Also can contain
522
+ custom levels.
523
+
524
+ Returns:
525
+ A Logger instance with the specified configuration.
526
+ """
527
+ if name is None:
528
+ frame = inspect.currentframe()
529
+ if frame and frame.f_back:
530
+ name = frame.f_back.f_code.co_name
531
+ else:
532
+ name = "logger"
533
+
534
+ return Logger(name, level, rich, display_all, level_styles=levels)
@@ -0,0 +1,43 @@
1
+ """hammad.pydantic
2
+
3
+ Contains both models and pydantic **specific** utiltiies / resources
4
+ meant for general case usage."""
5
+
6
+ from typing import TYPE_CHECKING
7
+ from ..based.utils import auto_create_lazy_loader
8
+
9
+ if TYPE_CHECKING:
10
+ from .converters import (
11
+ convert_to_pydantic_model,
12
+ convert_to_pydantic_field,
13
+ create_confirmation_pydantic_model,
14
+ create_selection_pydantic_model,
15
+ )
16
+ from .models import (
17
+ FastModel,
18
+ FunctionModel,
19
+ ArbitraryModel,
20
+ CacheableModel,
21
+ SubscriptableModel,
22
+ )
23
+
24
+
25
+ __all__ = (
26
+ "convert_to_pydantic_model",
27
+ "convert_to_pydantic_field",
28
+ "create_confirmation_pydantic_model",
29
+ "create_selection_pydantic_model",
30
+ "FastModel",
31
+ "FunctionModel",
32
+ "ArbitraryModel",
33
+ "CacheableModel",
34
+ "SubscriptableModel",
35
+ )
36
+
37
+
38
+ __getattr__ = auto_create_lazy_loader(__all__)
39
+
40
+
41
+ def __dir__() -> list[str]:
42
+ """Get the attributes of the pydantic module."""
43
+ return list(__all__)
@@ -27,7 +27,8 @@ from typing import (
27
27
  cast,
28
28
  )
29
29
  from pydantic import BaseModel, Field, create_model
30
- from ...cache import cached
30
+
31
+ from ..cache._cache import cached
31
32
 
32
33
  logger = logging.getLogger(__name__)
33
34
 
@@ -0,0 +1,28 @@
1
+ """hammad.pydantic.models"""
2
+
3
+ from typing import TYPE_CHECKING
4
+ from ...based.utils import auto_create_lazy_loader
5
+
6
+ if TYPE_CHECKING:
7
+ from .arbitrary_model import ArbitraryModel
8
+ from .cacheable_model import CacheableModel
9
+ from .fast_model import FastModel
10
+ from .function_model import FunctionModel
11
+ from .subscriptable_model import SubscriptableModel
12
+
13
+
14
+ __all__ = (
15
+ "ArbitraryModel",
16
+ "CacheableModel",
17
+ "FastModel",
18
+ "FunctionModel",
19
+ "SubscriptableModel",
20
+ )
21
+
22
+
23
+ __getattr__ = auto_create_lazy_loader(__all__)
24
+
25
+
26
+ def __dir__() -> list[str]:
27
+ """Get the attributes of the models module."""
28
+ return list(__all__)
@@ -0,0 +1,46 @@
1
+ """hammad.pydantic.models.arbitrary_model"""
2
+
3
+ from typing import Any, Dict
4
+ from pydantic import ConfigDict
5
+
6
+ from .subscriptable_model import SubscriptableModel
7
+
8
+ __all__ = ("ArbitraryModel",)
9
+
10
+
11
+ class ArbitraryModel(SubscriptableModel):
12
+ """
13
+ A model that allows dynamic field assignment and access.
14
+ Perfect for handling arbitrary JSON data or when schema is unknown at compile time.
15
+
16
+ Usage:
17
+ >>> data = ArbitraryModel()
18
+ >>> data.name = "John"
19
+ >>> data.age = 30
20
+ >>> data.metadata = {"key": "value"}
21
+ >>> print(data.name) # John
22
+ >>> print(data["age"]) # 30
23
+ """
24
+
25
+ model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
26
+
27
+ def __init__(self, **data: Any):
28
+ super().__init__(**data)
29
+ # Store extra fields for easy access
30
+ self._arbitrary_fields: Dict[str, Any] = {}
31
+
32
+ def __setattr__(self, name: str, value: Any) -> None:
33
+ if name.startswith("_") or name in self.__class__.model_fields:
34
+ super().__setattr__(name, value)
35
+ else:
36
+ # Store in dynamic fields and set normally
37
+ if hasattr(self, "_arbitrary_fields"):
38
+ self._arbitrary_fields[name] = value
39
+ super().__setattr__(name, value)
40
+
41
+ def to_dict(self) -> Dict[str, Any]:
42
+ """Convert to dictionary including all dynamic fields."""
43
+ result = self.model_dump()
44
+ if hasattr(self, "_arbitrary_fields"):
45
+ result.update(self._arbitrary_fields)
46
+ return result