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.
- hammad/__init__.py +169 -56
- hammad/_core/__init__.py +1 -0
- hammad/_core/_utils/__init__.py +4 -0
- hammad/_core/_utils/_import_utils.py +182 -0
- hammad/ai/__init__.py +59 -0
- hammad/ai/_utils.py +142 -0
- hammad/ai/completions/__init__.py +44 -0
- hammad/ai/completions/client.py +729 -0
- hammad/ai/completions/create.py +686 -0
- hammad/ai/completions/types.py +711 -0
- hammad/ai/completions/utils.py +374 -0
- hammad/ai/embeddings/__init__.py +35 -0
- hammad/ai/embeddings/client/__init__.py +1 -0
- hammad/ai/embeddings/client/base_embeddings_client.py +26 -0
- hammad/ai/embeddings/client/fastembed_text_embeddings_client.py +200 -0
- hammad/ai/embeddings/client/litellm_embeddings_client.py +288 -0
- hammad/ai/embeddings/create.py +159 -0
- hammad/ai/embeddings/types.py +69 -0
- hammad/base/__init__.py +35 -0
- hammad/{based → base}/fields.py +23 -23
- hammad/{based → base}/model.py +124 -14
- hammad/base/utils.py +280 -0
- hammad/cache/__init__.py +30 -12
- hammad/cache/base_cache.py +181 -0
- hammad/cache/cache.py +169 -0
- hammad/cache/decorators.py +261 -0
- hammad/cache/file_cache.py +80 -0
- hammad/cache/ttl_cache.py +74 -0
- hammad/cli/__init__.py +10 -2
- hammad/cli/{styles/animations.py → animations.py} +79 -23
- hammad/cli/{plugins/__init__.py → plugins.py} +85 -90
- hammad/cli/styles/__init__.py +50 -0
- hammad/cli/styles/settings.py +4 -0
- hammad/configuration/__init__.py +35 -0
- hammad/{data/types/files → configuration}/configuration.py +96 -7
- hammad/data/__init__.py +14 -26
- hammad/data/collections/__init__.py +4 -2
- hammad/data/collections/collection.py +300 -75
- hammad/data/collections/vector_collection.py +118 -12
- hammad/data/databases/__init__.py +2 -2
- hammad/data/databases/database.py +383 -32
- hammad/json/__init__.py +2 -2
- hammad/logging/__init__.py +13 -5
- hammad/logging/decorators.py +404 -2
- hammad/logging/logger.py +442 -22
- hammad/multimodal/__init__.py +24 -0
- hammad/{data/types/files → multimodal}/audio.py +21 -6
- hammad/{data/types/files → multimodal}/image.py +5 -5
- hammad/multithreading/__init__.py +304 -0
- hammad/pydantic/__init__.py +2 -2
- hammad/pydantic/converters.py +1 -1
- hammad/pydantic/models/__init__.py +2 -2
- hammad/text/__init__.py +59 -14
- hammad/text/converters.py +723 -0
- hammad/text/{utils/markdown/formatting.py → markdown.py} +25 -23
- hammad/text/text.py +12 -14
- hammad/types/__init__.py +11 -0
- hammad/{data/types/files → types}/file.py +18 -18
- hammad/typing/__init__.py +138 -84
- hammad/web/__init__.py +3 -2
- hammad/web/models.py +245 -0
- hammad/web/search/client.py +75 -23
- hammad/web/utils.py +14 -5
- hammad/yaml/__init__.py +2 -2
- hammad/yaml/converters.py +1 -1
- {hammad_python-0.0.11.dist-info → hammad_python-0.0.13.dist-info}/METADATA +4 -1
- hammad_python-0.0.13.dist-info/RECORD +85 -0
- hammad/based/__init__.py +0 -52
- hammad/based/utils.py +0 -455
- hammad/cache/_cache.py +0 -746
- hammad/data/types/__init__.py +0 -33
- hammad/data/types/files/__init__.py +0 -1
- hammad/data/types/files/document.py +0 -195
- hammad/text/utils/__init__.py +0 -1
- hammad/text/utils/converters.py +0 -229
- hammad/text/utils/markdown/__init__.py +0 -1
- hammad/text/utils/markdown/converters.py +0 -506
- hammad_python-0.0.11.dist-info/RECORD +0 -65
- {hammad_python-0.0.11.dist-info → hammad_python-0.0.13.dist-info}/WHEEL +0 -0
- {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
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
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=
|
420
|
+
show_time=self._date_format is not None,
|
304
421
|
show_path=False,
|
305
422
|
markup=True,
|
306
423
|
)
|
307
424
|
|
308
|
-
|
309
|
-
|
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
|
-
|
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
|
522
|
-
|
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(
|
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
|
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 =
|
18
|
-
_sample_rate: int | None =
|
19
|
-
_channels: int | None =
|
20
|
-
_format: str | 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
|
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 =
|
18
|
-
_height: int | None =
|
19
|
-
_format: str | 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:
|