hammad-python 0.0.11__py3-none-any.whl → 0.0.13__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 (80) hide show
  1. hammad/__init__.py +169 -56
  2. hammad/_core/__init__.py +1 -0
  3. hammad/_core/_utils/__init__.py +4 -0
  4. hammad/_core/_utils/_import_utils.py +182 -0
  5. hammad/ai/__init__.py +59 -0
  6. hammad/ai/_utils.py +142 -0
  7. hammad/ai/completions/__init__.py +44 -0
  8. hammad/ai/completions/client.py +729 -0
  9. hammad/ai/completions/create.py +686 -0
  10. hammad/ai/completions/types.py +711 -0
  11. hammad/ai/completions/utils.py +374 -0
  12. hammad/ai/embeddings/__init__.py +35 -0
  13. hammad/ai/embeddings/client/__init__.py +1 -0
  14. hammad/ai/embeddings/client/base_embeddings_client.py +26 -0
  15. hammad/ai/embeddings/client/fastembed_text_embeddings_client.py +200 -0
  16. hammad/ai/embeddings/client/litellm_embeddings_client.py +288 -0
  17. hammad/ai/embeddings/create.py +159 -0
  18. hammad/ai/embeddings/types.py +69 -0
  19. hammad/base/__init__.py +35 -0
  20. hammad/{based → base}/fields.py +23 -23
  21. hammad/{based → base}/model.py +124 -14
  22. hammad/base/utils.py +280 -0
  23. hammad/cache/__init__.py +30 -12
  24. hammad/cache/base_cache.py +181 -0
  25. hammad/cache/cache.py +169 -0
  26. hammad/cache/decorators.py +261 -0
  27. hammad/cache/file_cache.py +80 -0
  28. hammad/cache/ttl_cache.py +74 -0
  29. hammad/cli/__init__.py +10 -2
  30. hammad/cli/{styles/animations.py → animations.py} +79 -23
  31. hammad/cli/{plugins/__init__.py → plugins.py} +85 -90
  32. hammad/cli/styles/__init__.py +50 -0
  33. hammad/cli/styles/settings.py +4 -0
  34. hammad/configuration/__init__.py +35 -0
  35. hammad/{data/types/files → configuration}/configuration.py +96 -7
  36. hammad/data/__init__.py +14 -26
  37. hammad/data/collections/__init__.py +4 -2
  38. hammad/data/collections/collection.py +300 -75
  39. hammad/data/collections/vector_collection.py +118 -12
  40. hammad/data/databases/__init__.py +2 -2
  41. hammad/data/databases/database.py +383 -32
  42. hammad/json/__init__.py +2 -2
  43. hammad/logging/__init__.py +13 -5
  44. hammad/logging/decorators.py +404 -2
  45. hammad/logging/logger.py +442 -22
  46. hammad/multimodal/__init__.py +24 -0
  47. hammad/{data/types/files → multimodal}/audio.py +21 -6
  48. hammad/{data/types/files → multimodal}/image.py +5 -5
  49. hammad/multithreading/__init__.py +304 -0
  50. hammad/pydantic/__init__.py +2 -2
  51. hammad/pydantic/converters.py +1 -1
  52. hammad/pydantic/models/__init__.py +2 -2
  53. hammad/text/__init__.py +59 -14
  54. hammad/text/converters.py +723 -0
  55. hammad/text/{utils/markdown/formatting.py → markdown.py} +25 -23
  56. hammad/text/text.py +12 -14
  57. hammad/types/__init__.py +11 -0
  58. hammad/{data/types/files → types}/file.py +18 -18
  59. hammad/typing/__init__.py +138 -84
  60. hammad/web/__init__.py +3 -2
  61. hammad/web/models.py +245 -0
  62. hammad/web/search/client.py +75 -23
  63. hammad/web/utils.py +14 -5
  64. hammad/yaml/__init__.py +2 -2
  65. hammad/yaml/converters.py +1 -1
  66. {hammad_python-0.0.11.dist-info → hammad_python-0.0.13.dist-info}/METADATA +4 -1
  67. hammad_python-0.0.13.dist-info/RECORD +85 -0
  68. hammad/based/__init__.py +0 -52
  69. hammad/based/utils.py +0 -455
  70. hammad/cache/_cache.py +0 -746
  71. hammad/data/types/__init__.py +0 -33
  72. hammad/data/types/files/__init__.py +0 -1
  73. hammad/data/types/files/document.py +0 -195
  74. hammad/text/utils/__init__.py +0 -1
  75. hammad/text/utils/converters.py +0 -229
  76. hammad/text/utils/markdown/__init__.py +0 -1
  77. hammad/text/utils/markdown/converters.py +0 -506
  78. hammad_python-0.0.11.dist-info/RECORD +0 -65
  79. {hammad_python-0.0.11.dist-info → hammad_python-0.0.13.dist-info}/WHEEL +0 -0
  80. {hammad_python-0.0.11.dist-info → hammad_python-0.0.13.dist-info}/licenses/LICENSE +0 -0
hammad/logging/logger.py CHANGED
@@ -2,32 +2,49 @@
2
2
 
3
3
  import logging as _logging
4
4
  import inspect
5
+ from pathlib import Path
5
6
  from dataclasses import dataclass, field
6
7
  from typing import (
7
8
  Literal,
8
9
  TypeAlias,
9
- NamedTuple,
10
- ParamSpec,
11
- TypeVar,
12
10
  Dict,
13
11
  Optional,
14
12
  Any,
15
13
  Union,
14
+ List,
15
+ Callable,
16
+ Iterator,
16
17
  )
17
18
  from typing_extensions import TypedDict
19
+ from contextlib import contextmanager
18
20
 
19
21
  from rich import get_console as get_rich_console
20
22
  from rich.logging import RichHandler
23
+ from rich.progress import (
24
+ Progress,
25
+ TaskID,
26
+ SpinnerColumn,
27
+ TextColumn,
28
+ BarColumn,
29
+ TimeRemainingColumn,
30
+ )
31
+ from rich.spinner import Spinner
32
+ from rich.live import Live
21
33
 
22
34
  from ..cli.styles.types import (
23
35
  CLIStyleType,
24
36
  )
25
37
  from ..cli.styles.settings import CLIStyleRenderableSettings, CLIStyleBackgroundSettings
38
+ from ..cli.animations import (
39
+ animate_spinning,
40
+ )
26
41
 
27
42
  __all__ = (
28
43
  "Logger",
29
44
  "create_logger",
30
45
  "create_logger_level",
46
+ "LoggerConfig",
47
+ "FileConfig",
31
48
  )
32
49
 
33
50
 
@@ -40,10 +57,6 @@ LoggerLevelName: TypeAlias = Literal["debug", "info", "warning", "error", "criti
40
57
  """Literal type helper for logging levels."""
41
58
 
42
59
 
43
- _P = ParamSpec("_P")
44
- _R = TypeVar("_R")
45
-
46
-
47
60
  class LoggerLevelSettings(TypedDict, total=False):
48
61
  """Configuration dictionary for the display style of a
49
62
  single logging level."""
@@ -62,6 +75,71 @@ class LoggerLevelSettings(TypedDict, total=False):
62
75
  of the messages of this level. This includes the message itself."""
63
76
 
64
77
 
78
+ class FileConfig(TypedDict, total=False):
79
+ """Configuration for file logging."""
80
+
81
+ path: Union[str, Path]
82
+ """Path to the log file."""
83
+
84
+ mode: Literal["a", "w"]
85
+ """File mode - 'a' for append, 'w' for write (overwrites)."""
86
+
87
+ max_bytes: int
88
+ """Maximum size in bytes before rotation (0 for no rotation)."""
89
+
90
+ backup_count: int
91
+ """Number of backup files to keep during rotation."""
92
+
93
+ encoding: str
94
+ """File encoding (defaults to 'utf-8')."""
95
+
96
+ delay: bool
97
+ """Whether to delay file opening until first write."""
98
+
99
+ create_dirs: bool
100
+ """Whether to create parent directories if they don't exist."""
101
+
102
+
103
+ class LoggerConfig(TypedDict, total=False):
104
+ """Complete configuration for Logger initialization."""
105
+
106
+ name: str
107
+ """Logger name."""
108
+
109
+ level: Union[str, int]
110
+ """Logging level."""
111
+
112
+ rich: bool
113
+ """Whether to use rich formatting."""
114
+
115
+ display_all: bool
116
+ """Whether to display all log levels."""
117
+
118
+ level_styles: Dict[str, LoggerLevelSettings]
119
+ """Custom level styles."""
120
+
121
+ file: Union[str, Path, FileConfig]
122
+ """File logging configuration."""
123
+
124
+ files: List[Union[str, Path, FileConfig]]
125
+ """Multiple file destinations."""
126
+
127
+ format: str
128
+ """Custom log format string."""
129
+
130
+ date_format: str
131
+ """Date format for timestamps."""
132
+
133
+ json_logs: bool
134
+ """Whether to output structured JSON logs."""
135
+
136
+ console: bool
137
+ """Whether to log to console (default True)."""
138
+
139
+ handlers: List[_logging.Handler]
140
+ """Additional custom handlers."""
141
+
142
+
65
143
  # -----------------------------------------------------------------------------
66
144
  # Default Level Styles
67
145
  # -----------------------------------------------------------------------------
@@ -105,8 +183,6 @@ class RichLoggerFilter(_logging.Filter):
105
183
  if level_name in self.level_styles:
106
184
  style_config = self.level_styles[level_name]
107
185
 
108
- # We'll use a special attribute to store style config
109
- # The formatter/handler will use this to apply styling
110
186
  record._hammad_style_config = style_config
111
187
 
112
188
  return True
@@ -221,6 +297,13 @@ class Logger:
221
297
  rich: bool = True,
222
298
  display_all: bool = False,
223
299
  level_styles: Optional[Dict[str, LoggerLevelSettings]] = None,
300
+ file: Optional[Union[str, Path, FileConfig]] = None,
301
+ files: Optional[List[Union[str, Path, FileConfig]]] = None,
302
+ format: Optional[str] = None,
303
+ date_format: Optional[str] = None,
304
+ json_logs: bool = False,
305
+ console: bool = True,
306
+ handlers: Optional[List[_logging.Handler]] = None,
224
307
  ) -> None:
225
308
  """
226
309
  Initialize a new Logger instance.
@@ -231,6 +314,13 @@ class Logger:
231
314
  rich: Whether to use rich formatting for output
232
315
  display_all: If True, sets effective level to debug to show all messages
233
316
  level_styles: Custom level styles to override defaults
317
+ file: Single file configuration for logging
318
+ files: Multiple file configurations for logging
319
+ format: Custom log format string
320
+ date_format: Date format for timestamps
321
+ json_logs: Whether to output structured JSON logs
322
+ console: Whether to log to console (default True)
323
+ handlers: Additional custom handlers to add
234
324
  """
235
325
  logger_name = name or "hammad"
236
326
 
@@ -279,19 +369,46 @@ class Logger:
279
369
  # Create logger
280
370
  self._logger = _logging.getLogger(logger_name)
281
371
 
372
+ # Store configuration
373
+ self._file_config = file
374
+ self._files_config = files or []
375
+ self._format = format
376
+ self._date_format = date_format
377
+ self._json_logs = json_logs
378
+ self._console_enabled = console
379
+ self._rich_enabled = rich
380
+
282
381
  # Clear any existing handlers
283
382
  if self._logger.hasHandlers():
284
383
  self._logger.handlers.clear()
285
384
 
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)
385
+ # Setup handlers
386
+ self._setup_handlers(log_level)
387
+
388
+ # Add custom handlers if provided
389
+ if handlers:
390
+ for handler in handlers:
391
+ self._logger.addHandler(handler)
291
392
 
292
393
  self._logger.setLevel(log_level)
293
394
  self._logger.propagate = False
294
395
 
396
+ def _setup_handlers(self, log_level: int) -> None:
397
+ """Setup all handlers for the logger."""
398
+ # Console handler
399
+ if self._console_enabled:
400
+ if self._rich_enabled:
401
+ self._setup_rich_handler(log_level)
402
+ else:
403
+ self._setup_standard_handler(log_level)
404
+
405
+ # File handlers
406
+ if self._file_config:
407
+ self._setup_file_handler(self._file_config, log_level)
408
+
409
+ for file_config in self._files_config:
410
+ self._setup_file_handler(file_config, log_level)
411
+
295
412
  def _setup_rich_handler(self, log_level: int) -> None:
296
413
  """Setup rich handler for the logger."""
297
414
  console = get_rich_console()
@@ -300,14 +417,17 @@ class Logger:
300
417
  level=log_level,
301
418
  console=console,
302
419
  rich_tracebacks=True,
303
- show_time=False,
420
+ show_time=self._date_format is not None,
304
421
  show_path=False,
305
422
  markup=True,
306
423
  )
307
424
 
308
- formatter = RichLoggerFormatter(
309
- "| [bold]✼ {name}[/bold] - {message}", style="{"
310
- )
425
+ format_str = self._format or "| [bold]✼ {name}[/bold] - {message}"
426
+ formatter = RichLoggerFormatter(format_str, style="{")
427
+
428
+ if self._date_format:
429
+ formatter.datefmt = self._date_format
430
+
311
431
  handler.setFormatter(formatter)
312
432
 
313
433
  # Add our custom filter
@@ -318,12 +438,101 @@ class Logger:
318
438
  def _setup_standard_handler(self, log_level: int) -> None:
319
439
  """Setup standard handler for the logger."""
320
440
  handler = _logging.StreamHandler()
321
- formatter = _logging.Formatter("✼ {name} - {levelname} - {message}", style="{")
441
+
442
+ format_str = self._format or "✼ {name} - {levelname} - {message}"
443
+ if self._json_logs:
444
+ formatter = self._create_json_formatter()
445
+ else:
446
+ formatter = _logging.Formatter(format_str, style="{")
447
+ if self._date_format:
448
+ formatter.datefmt = self._date_format
449
+
450
+ handler.setFormatter(formatter)
451
+ handler.setLevel(log_level)
452
+
453
+ self._logger.addHandler(handler)
454
+
455
+ def _setup_file_handler(
456
+ self, file_config: Union[str, Path, FileConfig], log_level: int
457
+ ) -> None:
458
+ """Setup file handler for the logger."""
459
+ import logging.handlers
460
+
461
+ # Parse file configuration
462
+ if isinstance(file_config, (str, Path)):
463
+ config: FileConfig = {"path": file_config}
464
+ else:
465
+ config = file_config.copy()
466
+
467
+ file_path = Path(config["path"])
468
+
469
+ # Create directories if needed
470
+ if config.get("create_dirs", True):
471
+ file_path.parent.mkdir(parents=True, exist_ok=True)
472
+
473
+ # Determine handler type
474
+ max_bytes = config.get("max_bytes", 0)
475
+ backup_count = config.get("backup_count", 0)
476
+
477
+ if max_bytes > 0:
478
+ # Rotating file handler
479
+ handler = logging.handlers.RotatingFileHandler(
480
+ filename=str(file_path),
481
+ mode=config.get("mode", "a"),
482
+ maxBytes=max_bytes,
483
+ backupCount=backup_count,
484
+ encoding=config.get("encoding", "utf-8"),
485
+ delay=config.get("delay", False),
486
+ )
487
+ else:
488
+ # Regular file handler
489
+ handler = _logging.FileHandler(
490
+ filename=str(file_path),
491
+ mode=config.get("mode", "a"),
492
+ encoding=config.get("encoding", "utf-8"),
493
+ delay=config.get("delay", False),
494
+ )
495
+
496
+ # Set formatter
497
+ if self._json_logs:
498
+ formatter = self._create_json_formatter()
499
+ else:
500
+ format_str = self._format or "[{asctime}] {name} - {levelname} - {message}"
501
+ formatter = _logging.Formatter(format_str, style="{")
502
+ if self._date_format:
503
+ formatter.datefmt = self._date_format
504
+
322
505
  handler.setFormatter(formatter)
323
506
  handler.setLevel(log_level)
324
507
 
325
508
  self._logger.addHandler(handler)
326
509
 
510
+ def _create_json_formatter(self) -> _logging.Formatter:
511
+ """Create a JSON formatter for structured logging."""
512
+ import json
513
+ import datetime
514
+
515
+ class JSONFormatter(_logging.Formatter):
516
+ def format(self, record):
517
+ log_entry = {
518
+ "timestamp": datetime.datetime.fromtimestamp(
519
+ record.created
520
+ ).isoformat(),
521
+ "level": record.levelname,
522
+ "logger": record.name,
523
+ "message": record.getMessage(),
524
+ "module": record.module,
525
+ "function": record.funcName,
526
+ "line": record.lineno,
527
+ }
528
+
529
+ if record.exc_info:
530
+ log_entry["exception"] = self.formatException(record.exc_info)
531
+
532
+ return json.dumps(log_entry)
533
+
534
+ return JSONFormatter()
535
+
327
536
  def add_level(
328
537
  self, name: str, value: int, style: Optional[LoggerLevelSettings] = None
329
538
  ) -> None:
@@ -455,6 +664,191 @@ class Logger:
455
664
  """Get the underlying logging.Logger instance."""
456
665
  return self._logger
457
666
 
667
+ @contextmanager
668
+ def track(
669
+ self,
670
+ description: str = "Processing...",
671
+ total: Optional[int] = None,
672
+ spinner: Optional[str] = None,
673
+ show_progress: bool = True,
674
+ show_time: bool = True,
675
+ transient: bool = False,
676
+ ) -> Iterator[Union[TaskID, Callable[[str], None]]]:
677
+ """Context manager for tracking progress with rich progress bar or spinner.
678
+
679
+ Args:
680
+ description: Description of the task being tracked
681
+ total: Total number of steps (if None, uses spinner instead of progress bar)
682
+ spinner: Spinner style to use (if total is None)
683
+ show_progress: Whether to show progress percentage
684
+ show_time: Whether to show time remaining
685
+ transient: Whether to remove the progress display when done
686
+
687
+ Yields:
688
+ TaskID for progress updates or callable for spinner text updates
689
+
690
+ Examples:
691
+ # Progress bar
692
+ with logger.track("Processing files", total=100) as task:
693
+ for i in range(100):
694
+ # do work
695
+ task.advance(1)
696
+
697
+ # Spinner
698
+ with logger.track("Loading data") as update:
699
+ # do work
700
+ update("Still loading...")
701
+ """
702
+ console = get_rich_console()
703
+
704
+ if total is not None:
705
+ # Use progress bar
706
+ columns = [SpinnerColumn(), TextColumn("{task.description}")]
707
+ if show_progress:
708
+ columns.extend(
709
+ [BarColumn(), "[progress.percentage]{task.percentage:>3.0f}%"]
710
+ )
711
+ if show_time:
712
+ columns.append(TimeRemainingColumn())
713
+
714
+ with Progress(*columns, console=console, transient=transient) as progress:
715
+ task_id = progress.add_task(description, total=total)
716
+
717
+ class TaskWrapper:
718
+ def __init__(self, progress_obj, task_id):
719
+ self.progress = progress_obj
720
+ self.task_id = task_id
721
+
722
+ def advance(self, advance: int = 1) -> None:
723
+ self.progress.advance(self.task_id, advance)
724
+
725
+ def update(self, **kwargs) -> None:
726
+ self.progress.update(self.task_id, **kwargs)
727
+
728
+ yield TaskWrapper(progress, task_id)
729
+ else:
730
+ # Use spinner
731
+ spinner_obj = Spinner(spinner or "dots", text=description)
732
+
733
+ with Live(spinner_obj, console=console, transient=transient) as live:
734
+
735
+ def update_text(new_text: str) -> None:
736
+ spinner_obj.text = new_text
737
+ live.refresh()
738
+
739
+ yield update_text
740
+
741
+ def trace_function(self, *args, **kwargs):
742
+ """Apply function tracing decorator. Imports from decorators module."""
743
+ from .decorators import trace_function as _trace_function
744
+
745
+ return _trace_function(logger=self, *args, **kwargs)
746
+
747
+ def trace_cls(self, *args, **kwargs):
748
+ """Apply class tracing decorator. Imports from decorators module."""
749
+ from .decorators import trace_cls as _trace_cls
750
+
751
+ return _trace_cls(logger=self, *args, **kwargs)
752
+
753
+ def trace(self, *args, **kwargs):
754
+ """Apply universal tracing decorator. Imports from decorators module."""
755
+ from .decorators import trace as _trace
756
+
757
+ return _trace(logger=self, *args, **kwargs)
758
+
759
+ def animate_spinning(
760
+ self,
761
+ text: str,
762
+ duration: Optional[float] = None,
763
+ frames: Optional[List[str]] = None,
764
+ speed: float = 0.1,
765
+ level: LoggerLevelName = "info",
766
+ ) -> None:
767
+ """Display spinning animation with logging.
768
+
769
+ Args:
770
+ text: Text to display with spinner
771
+ duration: Duration to run animation (defaults to 2.0)
772
+ frames: Custom spinner frames
773
+ speed: Speed of animation
774
+ level: Log level to use
775
+ """
776
+ self.log(level, f"Starting: {text}")
777
+ animate_spinning(
778
+ text,
779
+ duration=duration,
780
+ frames=frames,
781
+ speed=speed,
782
+ )
783
+ self.log(level, f"Completed: {text}")
784
+
785
+ def add_file(
786
+ self,
787
+ file_config: Union[str, Path, FileConfig],
788
+ level: Optional[Union[str, int]] = None,
789
+ ) -> None:
790
+ """Add a new file handler to the logger.
791
+
792
+ Args:
793
+ file_config: File configuration
794
+ level: Optional level for this handler (uses logger level if None)
795
+ """
796
+ handler_level = level or self._logger.level
797
+ if isinstance(handler_level, str):
798
+ level_map = {
799
+ "debug": _logging.DEBUG,
800
+ "info": _logging.INFO,
801
+ "warning": _logging.WARNING,
802
+ "error": _logging.ERROR,
803
+ "critical": _logging.CRITICAL,
804
+ }
805
+ handler_level = level_map.get(handler_level.lower(), _logging.WARNING)
806
+
807
+ self._setup_file_handler(file_config, handler_level)
808
+
809
+ def remove_handlers(self, handler_types: Optional[List[str]] = None) -> None:
810
+ """Remove handlers from the logger.
811
+
812
+ Args:
813
+ handler_types: List of handler type names to remove.
814
+ If None, removes all handlers.
815
+ Options: ['file', 'console', 'rich', 'rotating']
816
+ """
817
+ if handler_types is None:
818
+ self._logger.handlers.clear()
819
+ return
820
+
821
+ handlers_to_remove = []
822
+ for handler in self._logger.handlers:
823
+ handler_type = type(handler).__name__.lower()
824
+
825
+ if any(ht in handler_type for ht in handler_types):
826
+ handlers_to_remove.append(handler)
827
+
828
+ for handler in handlers_to_remove:
829
+ self._logger.removeHandler(handler)
830
+
831
+ def get_file_paths(self) -> List[Path]:
832
+ """Get all file paths being logged to."""
833
+ file_paths = []
834
+
835
+ for handler in self._logger.handlers:
836
+ if hasattr(handler, "baseFilename"):
837
+ file_paths.append(Path(handler.baseFilename))
838
+
839
+ return file_paths
840
+
841
+ def flush(self) -> None:
842
+ """Flush all handlers."""
843
+ for handler in self._logger.handlers:
844
+ handler.flush()
845
+
846
+ def close(self) -> None:
847
+ """Close all handlers and cleanup resources."""
848
+ for handler in self._logger.handlers[:]:
849
+ handler.close()
850
+ self._logger.removeHandler(handler)
851
+
458
852
 
459
853
  # -----------------------------------------------------------------------------
460
854
  # Factory
@@ -509,6 +903,13 @@ def create_logger(
509
903
  rich: bool = True,
510
904
  display_all: bool = False,
511
905
  levels: Optional[Dict[LoggerLevelName, LoggerLevelSettings]] = None,
906
+ file: Optional[Union[str, Path, FileConfig]] = None,
907
+ files: Optional[List[Union[str, Path, FileConfig]]] = None,
908
+ format: Optional[str] = None,
909
+ date_format: Optional[str] = None,
910
+ json_logs: bool = False,
911
+ console: bool = True,
912
+ handlers: Optional[List[_logging.Handler]] = None,
512
913
  ) -> Logger:
513
914
  """
514
915
  Get a logger instance.
@@ -518,8 +919,14 @@ def create_logger(
518
919
  level: Logging level. If None, defaults to "debug" if display_all else "warning"
519
920
  rich: Whether to use rich formatting for output
520
921
  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.
922
+ levels: Custom level styles to override defaults
923
+ file: Single file configuration for logging
924
+ files: Multiple file configurations for logging
925
+ format: Custom log format string
926
+ date_format: Date format for timestamps
927
+ json_logs: Whether to output structured JSON logs
928
+ console: Whether to log to console (default True)
929
+ handlers: Additional custom handlers to add
523
930
 
524
931
  Returns:
525
932
  A Logger instance with the specified configuration.
@@ -531,4 +938,17 @@ def create_logger(
531
938
  else:
532
939
  name = "logger"
533
940
 
534
- return Logger(name, level, rich, display_all, level_styles=levels)
941
+ return Logger(
942
+ name=name,
943
+ level=level,
944
+ rich=rich,
945
+ display_all=display_all,
946
+ level_styles=levels,
947
+ file=file,
948
+ files=files,
949
+ format=format,
950
+ date_format=date_format,
951
+ json_logs=json_logs,
952
+ console=console,
953
+ handlers=handlers,
954
+ )
@@ -0,0 +1,24 @@
1
+ """hammad.multimodal
2
+
3
+ Contains types and model like objects for working with various
4
+ types of multimodal data."""
5
+
6
+ from typing import TYPE_CHECKING
7
+ from .._core._utils._import_utils import _auto_create_getattr_loader
8
+
9
+ if TYPE_CHECKING:
10
+ from .image import Image
11
+ from .audio import Audio
12
+
13
+
14
+ __all__ = (
15
+ "Image",
16
+ "Audio",
17
+ )
18
+
19
+
20
+ __getattr__ = _auto_create_getattr_loader(__all__)
21
+
22
+
23
+ def __dir__() -> list[str]:
24
+ return list(__all__)
@@ -3,8 +3,8 @@
3
3
  import httpx
4
4
  from typing import Self
5
5
 
6
- from .file import File, FileSource
7
- from ....based.fields import basedfield
6
+ from ..types.file import File, FileSource
7
+ from ..base.fields import field
8
8
 
9
9
  __all__ = ("Audio",)
10
10
 
@@ -14,10 +14,10 @@ class Audio(File):
14
14
  or bytes."""
15
15
 
16
16
  # Audio-specific metadata
17
- _duration: float | None = basedfield(default=None)
18
- _sample_rate: int | None = basedfield(default=None)
19
- _channels: int | None = basedfield(default=None)
20
- _format: str | None = basedfield(default=None)
17
+ _duration: float | None = field(default=None)
18
+ _sample_rate: int | None = field(default=None)
19
+ _channels: int | None = field(default=None)
20
+ _format: str | None = field(default=None)
21
21
 
22
22
  @property
23
23
  def is_valid_audio(self) -> bool:
@@ -32,6 +32,21 @@ class Audio(File):
32
32
  self._format = self.type.split("/")[-1].upper()
33
33
  return self._format
34
34
 
35
+ @property
36
+ def duration(self) -> float | None:
37
+ """Get the duration of the audio file in seconds."""
38
+ return self._duration
39
+
40
+ @property
41
+ def sample_rate(self) -> int | None:
42
+ """Get the sample rate of the audio file in Hz."""
43
+ return self._sample_rate
44
+
45
+ @property
46
+ def channels(self) -> int | None:
47
+ """Get the number of channels in the audio file."""
48
+ return self._channels
49
+
35
50
  @classmethod
36
51
  def from_url(
37
52
  cls,
@@ -3,8 +3,8 @@
3
3
  import httpx
4
4
  from typing import Self
5
5
 
6
- from .file import File, FileSource
7
- from ....based.fields import basedfield
6
+ from ..types.file import File, FileSource
7
+ from ..base.fields import field
8
8
 
9
9
  __all__ = ("Image",)
10
10
 
@@ -14,9 +14,9 @@ class Image(File):
14
14
  or bytes."""
15
15
 
16
16
  # Image-specific metadata
17
- _width: int | None = basedfield(default=None)
18
- _height: int | None = basedfield(default=None)
19
- _format: str | None = basedfield(default=None)
17
+ _width: int | None = field(default=None)
18
+ _height: int | None = field(default=None)
19
+ _format: str | None = field(default=None)
20
20
 
21
21
  @property
22
22
  def is_valid_image(self) -> bool: