hammad-python 0.0.30__py3-none-any.whl → 0.0.31__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 (137) hide show
  1. ham/__init__.py +10 -0
  2. {hammad_python-0.0.30.dist-info → hammad_python-0.0.31.dist-info}/METADATA +6 -32
  3. hammad_python-0.0.31.dist-info/RECORD +6 -0
  4. hammad/__init__.py +0 -84
  5. hammad/_internal.py +0 -256
  6. hammad/_main.py +0 -226
  7. hammad/cache/__init__.py +0 -40
  8. hammad/cache/base_cache.py +0 -181
  9. hammad/cache/cache.py +0 -169
  10. hammad/cache/decorators.py +0 -261
  11. hammad/cache/file_cache.py +0 -80
  12. hammad/cache/ttl_cache.py +0 -74
  13. hammad/cli/__init__.py +0 -33
  14. hammad/cli/animations.py +0 -573
  15. hammad/cli/plugins.py +0 -867
  16. hammad/cli/styles/__init__.py +0 -55
  17. hammad/cli/styles/settings.py +0 -139
  18. hammad/cli/styles/types.py +0 -358
  19. hammad/cli/styles/utils.py +0 -634
  20. hammad/data/__init__.py +0 -90
  21. hammad/data/collections/__init__.py +0 -49
  22. hammad/data/collections/collection.py +0 -326
  23. hammad/data/collections/indexes/__init__.py +0 -37
  24. hammad/data/collections/indexes/qdrant/__init__.py +0 -1
  25. hammad/data/collections/indexes/qdrant/index.py +0 -723
  26. hammad/data/collections/indexes/qdrant/settings.py +0 -94
  27. hammad/data/collections/indexes/qdrant/utils.py +0 -210
  28. hammad/data/collections/indexes/tantivy/__init__.py +0 -1
  29. hammad/data/collections/indexes/tantivy/index.py +0 -426
  30. hammad/data/collections/indexes/tantivy/settings.py +0 -40
  31. hammad/data/collections/indexes/tantivy/utils.py +0 -176
  32. hammad/data/configurations/__init__.py +0 -35
  33. hammad/data/configurations/configuration.py +0 -564
  34. hammad/data/models/__init__.py +0 -50
  35. hammad/data/models/extensions/__init__.py +0 -4
  36. hammad/data/models/extensions/pydantic/__init__.py +0 -42
  37. hammad/data/models/extensions/pydantic/converters.py +0 -759
  38. hammad/data/models/fields.py +0 -546
  39. hammad/data/models/model.py +0 -1078
  40. hammad/data/models/utils.py +0 -280
  41. hammad/data/sql/__init__.py +0 -24
  42. hammad/data/sql/database.py +0 -576
  43. hammad/data/sql/types.py +0 -127
  44. hammad/data/types/__init__.py +0 -75
  45. hammad/data/types/file.py +0 -431
  46. hammad/data/types/multimodal/__init__.py +0 -36
  47. hammad/data/types/multimodal/audio.py +0 -200
  48. hammad/data/types/multimodal/image.py +0 -182
  49. hammad/data/types/text.py +0 -1308
  50. hammad/formatting/__init__.py +0 -33
  51. hammad/formatting/json/__init__.py +0 -27
  52. hammad/formatting/json/converters.py +0 -158
  53. hammad/formatting/text/__init__.py +0 -63
  54. hammad/formatting/text/converters.py +0 -723
  55. hammad/formatting/text/markdown.py +0 -131
  56. hammad/formatting/yaml/__init__.py +0 -26
  57. hammad/formatting/yaml/converters.py +0 -5
  58. hammad/genai/__init__.py +0 -217
  59. hammad/genai/a2a/__init__.py +0 -32
  60. hammad/genai/a2a/workers.py +0 -552
  61. hammad/genai/agents/__init__.py +0 -59
  62. hammad/genai/agents/agent.py +0 -1973
  63. hammad/genai/agents/run.py +0 -1024
  64. hammad/genai/agents/types/__init__.py +0 -42
  65. hammad/genai/agents/types/agent_context.py +0 -13
  66. hammad/genai/agents/types/agent_event.py +0 -128
  67. hammad/genai/agents/types/agent_hooks.py +0 -220
  68. hammad/genai/agents/types/agent_messages.py +0 -31
  69. hammad/genai/agents/types/agent_response.py +0 -125
  70. hammad/genai/agents/types/agent_stream.py +0 -327
  71. hammad/genai/graphs/__init__.py +0 -125
  72. hammad/genai/graphs/_utils.py +0 -190
  73. hammad/genai/graphs/base.py +0 -1828
  74. hammad/genai/graphs/plugins.py +0 -316
  75. hammad/genai/graphs/types.py +0 -638
  76. hammad/genai/models/__init__.py +0 -1
  77. hammad/genai/models/embeddings/__init__.py +0 -43
  78. hammad/genai/models/embeddings/model.py +0 -226
  79. hammad/genai/models/embeddings/run.py +0 -163
  80. hammad/genai/models/embeddings/types/__init__.py +0 -37
  81. hammad/genai/models/embeddings/types/embedding_model_name.py +0 -75
  82. hammad/genai/models/embeddings/types/embedding_model_response.py +0 -76
  83. hammad/genai/models/embeddings/types/embedding_model_run_params.py +0 -66
  84. hammad/genai/models/embeddings/types/embedding_model_settings.py +0 -47
  85. hammad/genai/models/language/__init__.py +0 -57
  86. hammad/genai/models/language/model.py +0 -1098
  87. hammad/genai/models/language/run.py +0 -878
  88. hammad/genai/models/language/types/__init__.py +0 -40
  89. hammad/genai/models/language/types/language_model_instructor_mode.py +0 -47
  90. hammad/genai/models/language/types/language_model_messages.py +0 -28
  91. hammad/genai/models/language/types/language_model_name.py +0 -239
  92. hammad/genai/models/language/types/language_model_request.py +0 -127
  93. hammad/genai/models/language/types/language_model_response.py +0 -217
  94. hammad/genai/models/language/types/language_model_response_chunk.py +0 -56
  95. hammad/genai/models/language/types/language_model_settings.py +0 -89
  96. hammad/genai/models/language/types/language_model_stream.py +0 -600
  97. hammad/genai/models/language/utils/__init__.py +0 -28
  98. hammad/genai/models/language/utils/requests.py +0 -421
  99. hammad/genai/models/language/utils/structured_outputs.py +0 -135
  100. hammad/genai/models/model_provider.py +0 -4
  101. hammad/genai/models/multimodal.py +0 -47
  102. hammad/genai/models/reranking.py +0 -26
  103. hammad/genai/types/__init__.py +0 -1
  104. hammad/genai/types/base.py +0 -215
  105. hammad/genai/types/history.py +0 -290
  106. hammad/genai/types/tools.py +0 -507
  107. hammad/logging/__init__.py +0 -35
  108. hammad/logging/decorators.py +0 -834
  109. hammad/logging/logger.py +0 -1018
  110. hammad/mcp/__init__.py +0 -53
  111. hammad/mcp/client/__init__.py +0 -35
  112. hammad/mcp/client/client.py +0 -624
  113. hammad/mcp/client/client_service.py +0 -400
  114. hammad/mcp/client/settings.py +0 -178
  115. hammad/mcp/servers/__init__.py +0 -26
  116. hammad/mcp/servers/launcher.py +0 -1161
  117. hammad/runtime/__init__.py +0 -32
  118. hammad/runtime/decorators.py +0 -142
  119. hammad/runtime/run.py +0 -299
  120. hammad/service/__init__.py +0 -49
  121. hammad/service/create.py +0 -527
  122. hammad/service/decorators.py +0 -283
  123. hammad/types.py +0 -288
  124. hammad/typing/__init__.py +0 -435
  125. hammad/web/__init__.py +0 -43
  126. hammad/web/http/__init__.py +0 -1
  127. hammad/web/http/client.py +0 -944
  128. hammad/web/models.py +0 -275
  129. hammad/web/openapi/__init__.py +0 -1
  130. hammad/web/openapi/client.py +0 -740
  131. hammad/web/search/__init__.py +0 -1
  132. hammad/web/search/client.py +0 -1023
  133. hammad/web/utils.py +0 -472
  134. hammad_python-0.0.30.dist-info/RECORD +0 -135
  135. {hammad → ham}/py.typed +0 -0
  136. {hammad_python-0.0.30.dist-info → hammad_python-0.0.31.dist-info}/WHEEL +0 -0
  137. {hammad_python-0.0.30.dist-info → hammad_python-0.0.31.dist-info}/licenses/LICENSE +0 -0
hammad/logging/logger.py DELETED
@@ -1,1018 +0,0 @@
1
- """hammad.logging.logger"""
2
-
3
- import logging as _logging
4
- import inspect
5
- from pathlib import Path
6
- from dataclasses import dataclass, field
7
- from typing import (
8
- Literal,
9
- TypeAlias,
10
- Dict,
11
- Optional,
12
- Any,
13
- Union,
14
- List,
15
- Callable,
16
- Iterator,
17
- )
18
- from typing_extensions import TypedDict
19
- from contextlib import contextmanager
20
-
21
- from rich import get_console as get_rich_console
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
33
-
34
- from ..cli.styles.types import (
35
- CLIStyleType,
36
- )
37
- from ..cli.styles.settings import CLIStyleRenderableSettings, CLIStyleBackgroundSettings
38
- from ..cli.animations import (
39
- animate_spinning,
40
- )
41
-
42
- __all__ = (
43
- "Logger",
44
- "create_logger",
45
- "create_logger_level",
46
- "LoggerConfig",
47
- "FileConfig",
48
- )
49
-
50
-
51
- # -----------------------------------------------------------------------------
52
- # Types
53
- # -----------------------------------------------------------------------------
54
-
55
-
56
- LoggerLevelName: TypeAlias = Literal["debug", "info", "warning", "error", "critical"]
57
- """Literal type helper for logging levels."""
58
-
59
-
60
- class LoggerLevelSettings(TypedDict, total=False):
61
- """Configuration dictionary for the display style of a
62
- single logging level."""
63
-
64
- title: CLIStyleType | CLIStyleRenderableSettings
65
- """Either a string tag or style settings for the title output
66
- of the messages of this level. This includes module name
67
- and level name."""
68
-
69
- message: CLIStyleType | CLIStyleRenderableSettings
70
- """Either a string tag or style settings for the message output
71
- of the messages of this level. This includes the message itself."""
72
-
73
- background: CLIStyleType | CLIStyleBackgroundSettings
74
- """Either a string tag or style settings for the background output
75
- of the messages of this level. This includes the message itself."""
76
-
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
-
143
- # -----------------------------------------------------------------------------
144
- # Default Level Styles
145
- # -----------------------------------------------------------------------------
146
-
147
- DEFAULT_LEVEL_STYLES: Dict[str, LoggerLevelSettings] = {
148
- "critical": {
149
- "message": "red bold",
150
- },
151
- "error": {
152
- "message": "red italic",
153
- },
154
- "warning": {
155
- "message": "yellow italic",
156
- },
157
- "info": {
158
- "message": "white",
159
- },
160
- "debug": {
161
- "message": "white italic dim",
162
- },
163
- }
164
-
165
-
166
- # -----------------------------------------------------------------------------
167
- # Logging Filter
168
- # -----------------------------------------------------------------------------
169
-
170
-
171
- class RichLoggerFilter(_logging.Filter):
172
- """Filter for applying rich styling to log messages based on level."""
173
-
174
- def __init__(self, level_styles: Dict[str, LoggerLevelSettings]):
175
- super().__init__()
176
- self.level_styles = level_styles
177
-
178
- def filter(self, record: _logging.LogRecord) -> bool:
179
- # Get the level name
180
- level_name = record.levelname.lower()
181
-
182
- # Check if we have custom styling for this level
183
- if level_name in self.level_styles:
184
- style_config = self.level_styles[level_name]
185
-
186
- record._hammad_style_config = style_config
187
-
188
- return True
189
-
190
-
191
- # -----------------------------------------------------------------------------
192
- # Custom Rich Formatter
193
- # -----------------------------------------------------------------------------
194
-
195
-
196
- class RichLoggerFormatter(_logging.Formatter):
197
- """Custom formatter that applies rich styling."""
198
-
199
- def __init__(self, *args, **kwargs):
200
- super().__init__(*args, **kwargs)
201
- self.console = get_rich_console()
202
-
203
- def formatMessage(self, record: _logging.LogRecord) -> str:
204
- """Override formatMessage to apply styling to different parts."""
205
- # Check if we have style configuration
206
- if hasattr(record, "_hammad_style_config"):
207
- style_config = record._hammad_style_config
208
-
209
- # Handle title styling (logger name)
210
- title_style = style_config.get("title", None)
211
- if title_style:
212
- if isinstance(title_style, str):
213
- # It's a color/style string tag
214
- record.name = f"[{title_style}]{record.name}[/{title_style}]"
215
- elif isinstance(title_style, dict):
216
- # It's a CLIStyleRenderableSettings dict
217
- style_str = self._build_renderable_style_string(title_style)
218
- if style_str:
219
- record.name = f"[{style_str}]{record.name}[/{style_str}]"
220
-
221
- # Handle message styling
222
- message_style = style_config.get("message", None)
223
- if message_style:
224
- if isinstance(message_style, str):
225
- # It's a color/style string tag
226
- record.message = (
227
- f"[{message_style}]{record.getMessage()}[/{message_style}]"
228
- )
229
- elif isinstance(message_style, dict):
230
- # It's a CLIStyleRenderableSettings dict
231
- style_str = self._build_renderable_style_string(message_style)
232
- if style_str:
233
- record.message = (
234
- f"[{style_str}]{record.getMessage()}[/{style_str}]"
235
- )
236
- else:
237
- record.message = record.getMessage()
238
- else:
239
- record.message = record.getMessage()
240
- else:
241
- record.message = record.getMessage()
242
-
243
- # Now format with the styled values
244
- formatted = self._style._fmt.format(**record.__dict__)
245
- return formatted if formatted != "None" else ""
246
-
247
- def _build_renderable_style_string(self, style_dict: dict) -> str:
248
- """Build a rich markup style string from a CLIStyleRenderableSettings dictionary."""
249
- style_parts = []
250
-
251
- # Handle all the style attributes from CLIStyleRenderableSettings
252
- for attr in [
253
- "bold",
254
- "italic",
255
- "dim",
256
- "underline",
257
- "strike",
258
- "blink",
259
- "blink2",
260
- "reverse",
261
- "conceal",
262
- "underline2",
263
- "frame",
264
- "encircle",
265
- "overline",
266
- ]:
267
- if style_dict.get(attr):
268
- style_parts.append(attr)
269
-
270
- return " ".join(style_parts) if style_parts else ""
271
-
272
-
273
- # -----------------------------------------------------------------------------
274
- # Logger
275
- # -----------------------------------------------------------------------------
276
-
277
-
278
- @dataclass
279
- class Logger:
280
- """Flexible logger with rich styling and custom level support."""
281
-
282
- _logger: _logging.Logger = field(init=False)
283
- """The underlying logging.Logger instance."""
284
-
285
- _level_styles: Dict[str, LoggerLevelSettings] = field(init=False)
286
- """Custom level styles."""
287
-
288
- _custom_levels: Dict[str, int] = field(init=False)
289
- """Custom logging levels."""
290
-
291
- _user_level: str = field(init=False)
292
- """User-specified logging level."""
293
-
294
- def __init__(
295
- self,
296
- name: Optional[str] = None,
297
- level: Optional[Union[LoggerLevelName, int]] = None,
298
- rich: bool = True,
299
- display_all: bool = False,
300
- level_styles: Optional[Dict[str, LoggerLevelSettings]] = None,
301
- file: Optional[Union[str, Path, FileConfig]] = None,
302
- files: Optional[List[Union[str, Path, FileConfig]]] = None,
303
- format: Optional[str] = None,
304
- date_format: Optional[str] = None,
305
- json_logs: bool = False,
306
- console: bool = True,
307
- handlers: Optional[List[_logging.Handler]] = None,
308
- ) -> None:
309
- """
310
- Initialize a new Logger instance.
311
-
312
- Args:
313
- name: Name for the logger. If None, defaults to "hammad"
314
- level: Logging level. If None, defaults to "debug" if display_all else "warning"
315
- rich: Whether to use rich formatting for output
316
- display_all: If True, sets effective level to debug to show all messages
317
- level_styles: Custom level styles to override defaults
318
- file: Single file configuration for logging
319
- files: Multiple file configurations for logging
320
- format: Custom log format string
321
- date_format: Date format for timestamps
322
- json_logs: Whether to output structured JSON logs
323
- console: Whether to log to console (default True)
324
- handlers: Additional custom handlers to add
325
- """
326
- logger_name = name or "hammad"
327
-
328
- # Initialize custom levels dict
329
- self._custom_levels = {}
330
-
331
- # Initialize level styles with defaults
332
- self._level_styles = DEFAULT_LEVEL_STYLES.copy()
333
- if level_styles:
334
- self._level_styles.update(level_styles)
335
-
336
- # Handle integer levels by converting to string names
337
- if isinstance(level, int):
338
- # Map standard logging levels to their names
339
- int_to_name = {
340
- _logging.DEBUG: "debug",
341
- _logging.INFO: "info",
342
- _logging.WARNING: "warning",
343
- _logging.ERROR: "error",
344
- _logging.CRITICAL: "critical",
345
- }
346
- level = int_to_name.get(level, "warning")
347
-
348
- self._user_level = level or "warning"
349
-
350
- if display_all:
351
- effective_level = "debug"
352
- else:
353
- effective_level = self._user_level
354
-
355
- # Standard level mapping
356
- level_map = {
357
- "debug": _logging.DEBUG,
358
- "info": _logging.INFO,
359
- "warning": _logging.WARNING,
360
- "error": _logging.ERROR,
361
- "critical": _logging.CRITICAL,
362
- }
363
-
364
- # Check if it's a custom level
365
- if effective_level.lower() in self._custom_levels:
366
- log_level = self._custom_levels[effective_level.lower()]
367
- else:
368
- log_level = level_map.get(effective_level.lower(), _logging.WARNING)
369
-
370
- # Create logger
371
- self._logger = _logging.getLogger(logger_name)
372
-
373
- # Store configuration
374
- self._file_config = file
375
- self._files_config = files or []
376
- self._format = format
377
- self._date_format = date_format
378
- self._json_logs = json_logs
379
- self._console_enabled = console
380
- self._rich_enabled = rich
381
-
382
- # Clear any existing handlers
383
- if self._logger.hasHandlers():
384
- self._logger.handlers.clear()
385
-
386
- # Setup handlers
387
- self._setup_handlers(log_level)
388
-
389
- # Add custom handlers if provided
390
- if handlers:
391
- for handler in handlers:
392
- self._logger.addHandler(handler)
393
-
394
- self._logger.setLevel(log_level)
395
- self._logger.propagate = False
396
-
397
- def _setup_handlers(self, log_level: int) -> None:
398
- """Setup all handlers for the logger."""
399
- # Console handler
400
- if self._console_enabled:
401
- if self._rich_enabled:
402
- self._setup_rich_handler(log_level)
403
- else:
404
- self._setup_standard_handler(log_level)
405
-
406
- # File handlers
407
- if self._file_config:
408
- self._setup_file_handler(self._file_config, log_level)
409
-
410
- for file_config in self._files_config:
411
- self._setup_file_handler(file_config, log_level)
412
-
413
- def _setup_rich_handler(self, log_level: int) -> None:
414
- """Setup rich handler for the logger."""
415
- console = get_rich_console()
416
-
417
- handler = RichHandler(
418
- level=log_level,
419
- console=console,
420
- rich_tracebacks=True,
421
- show_time=self._date_format is not None,
422
- show_path=False,
423
- markup=True,
424
- )
425
-
426
- format_str = self._format or "| [bold]✼ {name}[/bold] - {message}"
427
- formatter = RichLoggerFormatter(format_str, style="{")
428
-
429
- if self._date_format:
430
- formatter.datefmt = self._date_format
431
-
432
- handler.setFormatter(formatter)
433
-
434
- # Add our custom filter
435
- handler.addFilter(RichLoggerFilter(self._level_styles))
436
-
437
- self._logger.addHandler(handler)
438
-
439
- def _setup_standard_handler(self, log_level: int) -> None:
440
- """Setup standard handler for the logger."""
441
- handler = _logging.StreamHandler()
442
-
443
- format_str = self._format or "✼ {name} - {levelname} - {message}"
444
- if self._json_logs:
445
- formatter = self._create_json_formatter()
446
- else:
447
- formatter = _logging.Formatter(format_str, style="{")
448
- if self._date_format:
449
- formatter.datefmt = self._date_format
450
-
451
- handler.setFormatter(formatter)
452
- handler.setLevel(log_level)
453
-
454
- self._logger.addHandler(handler)
455
-
456
- def _setup_file_handler(
457
- self, file_config: Union[str, Path, FileConfig], log_level: int
458
- ) -> None:
459
- """Setup file handler for the logger."""
460
- import logging.handlers
461
-
462
- # Parse file configuration
463
- if isinstance(file_config, (str, Path)):
464
- config: FileConfig = {"path": file_config}
465
- else:
466
- config = file_config.copy()
467
-
468
- file_path = Path(config["path"])
469
-
470
- # Create directories if needed
471
- if config.get("create_dirs", True):
472
- file_path.parent.mkdir(parents=True, exist_ok=True)
473
-
474
- # Determine handler type
475
- max_bytes = config.get("max_bytes", 0)
476
- backup_count = config.get("backup_count", 0)
477
-
478
- if max_bytes > 0:
479
- # Rotating file handler
480
- handler = logging.handlers.RotatingFileHandler(
481
- filename=str(file_path),
482
- mode=config.get("mode", "a"),
483
- maxBytes=max_bytes,
484
- backupCount=backup_count,
485
- encoding=config.get("encoding", "utf-8"),
486
- delay=config.get("delay", False),
487
- )
488
- else:
489
- # Regular file handler
490
- handler = _logging.FileHandler(
491
- filename=str(file_path),
492
- mode=config.get("mode", "a"),
493
- encoding=config.get("encoding", "utf-8"),
494
- delay=config.get("delay", False),
495
- )
496
-
497
- # Set formatter
498
- if self._json_logs:
499
- formatter = self._create_json_formatter()
500
- else:
501
- format_str = self._format or "[{asctime}] {name} - {levelname} - {message}"
502
- formatter = _logging.Formatter(format_str, style="{")
503
- if self._date_format:
504
- formatter.datefmt = self._date_format
505
-
506
- handler.setFormatter(formatter)
507
- handler.setLevel(log_level)
508
-
509
- self._logger.addHandler(handler)
510
-
511
- def _create_json_formatter(self) -> _logging.Formatter:
512
- """Create a JSON formatter for structured logging."""
513
- import json
514
- import datetime
515
-
516
- class JSONFormatter(_logging.Formatter):
517
- def format(self, record):
518
- log_entry = {
519
- "timestamp": datetime.datetime.fromtimestamp(
520
- record.created
521
- ).isoformat(),
522
- "level": record.levelname,
523
- "logger": record.name,
524
- "message": record.getMessage(),
525
- "module": record.module,
526
- "function": record.funcName,
527
- "line": record.lineno,
528
- }
529
-
530
- if record.exc_info:
531
- log_entry["exception"] = self.formatException(record.exc_info)
532
-
533
- return json.dumps(log_entry)
534
-
535
- return JSONFormatter()
536
-
537
- def setLevel(
538
- self,
539
- level: Union[LoggerLevelName, int],
540
- ) -> None:
541
- """Set the logging level."""
542
- # Handle integer levels by converting to string names
543
- if isinstance(level, int):
544
- # Map standard logging levels to their names
545
- int_to_name = {
546
- _logging.DEBUG: "debug",
547
- _logging.INFO: "info",
548
- _logging.WARNING: "warning",
549
- _logging.ERROR: "error",
550
- _logging.CRITICAL: "critical",
551
- }
552
- level_str = int_to_name.get(level, "warning")
553
- else:
554
- level_str = level
555
-
556
- self._user_level = level_str
557
-
558
- # Standard level mapping
559
- level_map = {
560
- "debug": _logging.DEBUG,
561
- "info": _logging.INFO,
562
- "warning": _logging.WARNING,
563
- "error": _logging.ERROR,
564
- "critical": _logging.CRITICAL,
565
- }
566
-
567
- # Check custom levels first
568
- if level_str.lower() in self._custom_levels:
569
- log_level = self._custom_levels[level_str.lower()]
570
- else:
571
- log_level = level_map.get(level_str.lower(), _logging.WARNING)
572
-
573
- # Set the integer level on the logger and handlers
574
- self._logger.setLevel(log_level)
575
- for handler in self._logger.handlers:
576
- handler.setLevel(log_level)
577
-
578
- def add_level(
579
- self, name: str, value: int, style: Optional[LoggerLevelSettings] = None
580
- ) -> None:
581
- """
582
- Add a custom logging level.
583
-
584
- Args:
585
- name: Name of the custom level
586
- value: Numeric value for the level (should be unique)
587
- style: Optional style settings for the level
588
- """
589
- # Add to Python's logging module
590
- _logging.addLevelName(value, name.upper())
591
-
592
- # Store in our custom levels
593
- self._custom_levels[name.lower()] = value
594
-
595
- # Add style if provided
596
- if style:
597
- self._level_styles[name.lower()] = style
598
-
599
- # Update filters if using rich handler
600
- for handler in self._logger.handlers:
601
- if isinstance(handler, RichHandler):
602
- # Remove old filter and add new one with updated styles
603
- for f in handler.filters[:]:
604
- if isinstance(f, RichLoggerFilter):
605
- handler.removeFilter(f)
606
- handler.addFilter(RichLoggerFilter(self._level_styles))
607
-
608
- @property
609
- def level(self) -> str:
610
- """Get the current logging level."""
611
- return self._user_level
612
-
613
- @level.setter
614
- def level(self, value: Union[str, int]) -> None:
615
- """Set the logging level."""
616
- # Handle integer levels by converting to string names
617
- if isinstance(value, int):
618
- # Map standard logging levels to their names
619
- int_to_name = {
620
- _logging.DEBUG: "debug",
621
- _logging.INFO: "info",
622
- _logging.WARNING: "warning",
623
- _logging.ERROR: "error",
624
- _logging.CRITICAL: "critical",
625
- }
626
- value_str = int_to_name.get(value, "warning")
627
- else:
628
- value_str = value
629
-
630
- self._user_level = value_str
631
-
632
- # Standard level mapping
633
- level_map = {
634
- "debug": _logging.DEBUG,
635
- "info": _logging.INFO,
636
- "warning": _logging.WARNING,
637
- "error": _logging.ERROR,
638
- "critical": _logging.CRITICAL,
639
- }
640
-
641
- # Check custom levels
642
- if value_str.lower() in self._custom_levels:
643
- log_level = self._custom_levels[value_str.lower()]
644
- else:
645
- log_level = level_map.get(value_str.lower(), _logging.WARNING)
646
-
647
- # Update logger level
648
- self._logger.setLevel(log_level)
649
-
650
- # Update handler levels
651
- for handler in self._logger.handlers:
652
- handler.setLevel(log_level)
653
-
654
- # Convenience methods for standard logging levels
655
- def debug(self, message: str, *args: Any, **kwargs: Any) -> None:
656
- """Log a debug message."""
657
- self._logger.debug(message, *args, **kwargs)
658
-
659
- def info(self, message: str, *args: Any, **kwargs: Any) -> None:
660
- """Log an info message."""
661
- self._logger.info(message, *args, **kwargs)
662
-
663
- def warning(self, message: str, *args: Any, **kwargs: Any) -> None:
664
- """Log a warning message."""
665
- self._logger.warning(message, *args, **kwargs)
666
-
667
- def error(self, message: str, *args: Any, **kwargs: Any) -> None:
668
- """Log an error message."""
669
- self._logger.error(message, *args, **kwargs)
670
-
671
- def critical(self, message: str, *args: Any, **kwargs: Any) -> None:
672
- """Log a critical message."""
673
- self._logger.critical(message, *args, **kwargs)
674
-
675
- def log(
676
- self, level: Union[str, int], message: str, *args: Any, **kwargs: Any
677
- ) -> None:
678
- """
679
- Log a message at the specified level.
680
-
681
- Args:
682
- level: The level to log at (can be standard or custom)
683
- message: The message to log
684
- *args: Additional positional arguments for the logger
685
- **kwargs: Additional keyword arguments for the logger
686
- """
687
- # Standard level mapping
688
- level_map = {
689
- "debug": _logging.DEBUG,
690
- "info": _logging.INFO,
691
- "warning": _logging.WARNING,
692
- "error": _logging.ERROR,
693
- "critical": _logging.CRITICAL,
694
- }
695
-
696
- # Handle integer levels
697
- if isinstance(level, int):
698
- # Use the integer level directly
699
- log_level = level
700
- else:
701
- # Check custom levels first
702
- if level.lower() in self._custom_levels:
703
- log_level = self._custom_levels[level.lower()]
704
- else:
705
- log_level = level_map.get(level.lower(), _logging.WARNING)
706
-
707
- self._logger.log(log_level, message, *args, **kwargs)
708
-
709
- @property
710
- def name(self) -> str:
711
- """Get the logger name."""
712
- return self._logger.name
713
-
714
- @property
715
- def handlers(self) -> list[_logging.Handler]:
716
- """Get the logger handlers."""
717
- return self._logger.handlers
718
-
719
- def get_logger(self) -> _logging.Logger:
720
- """Get the underlying logging.Logger instance."""
721
- return self._logger
722
-
723
- @contextmanager
724
- def track(
725
- self,
726
- description: str = "Processing...",
727
- total: Optional[int] = None,
728
- spinner: Optional[str] = None,
729
- show_progress: bool = True,
730
- show_time: bool = True,
731
- transient: bool = False,
732
- ) -> Iterator[Union[TaskID, Callable[[str], None]]]:
733
- """Context manager for tracking progress with rich progress bar or spinner.
734
-
735
- Args:
736
- description: Description of the task being tracked
737
- total: Total number of steps (if None, uses spinner instead of progress bar)
738
- spinner: Spinner style to use (if total is None)
739
- show_progress: Whether to show progress percentage
740
- show_time: Whether to show time remaining
741
- transient: Whether to remove the progress display when done
742
-
743
- Yields:
744
- TaskID for progress updates or callable for spinner text updates
745
-
746
- Examples:
747
- # Progress bar
748
- with logger.track("Processing files", total=100) as task:
749
- for i in range(100):
750
- # do work
751
- task.advance(1)
752
-
753
- # Spinner
754
- with logger.track("Loading data") as update:
755
- # do work
756
- update("Still loading...")
757
- """
758
- console = get_rich_console()
759
-
760
- if total is not None:
761
- # Use progress bar
762
- columns = [SpinnerColumn(), TextColumn("{task.description}")]
763
- if show_progress:
764
- columns.extend(
765
- [BarColumn(), "[progress.percentage]{task.percentage:>3.0f}%"]
766
- )
767
- if show_time:
768
- columns.append(TimeRemainingColumn())
769
-
770
- with Progress(*columns, console=console, transient=transient) as progress:
771
- task_id = progress.add_task(description, total=total)
772
-
773
- class TaskWrapper:
774
- def __init__(self, progress_obj, task_id):
775
- self.progress = progress_obj
776
- self.task_id = task_id
777
-
778
- def advance(self, advance: int = 1) -> None:
779
- self.progress.advance(self.task_id, advance)
780
-
781
- def update(self, **kwargs) -> None:
782
- self.progress.update(self.task_id, **kwargs)
783
-
784
- yield TaskWrapper(progress, task_id)
785
- else:
786
- # Use spinner
787
- spinner_obj = Spinner(spinner or "dots", text=description)
788
-
789
- with Live(spinner_obj, console=console, transient=transient) as live:
790
-
791
- def update_text(new_text: str) -> None:
792
- spinner_obj.text = new_text
793
- live.refresh()
794
-
795
- yield update_text
796
-
797
- def trace_function(self, *args, **kwargs):
798
- """Apply function tracing decorator. Imports from decorators module."""
799
- from .decorators import trace_function as _trace_function
800
-
801
- return _trace_function(logger=self, *args, **kwargs)
802
-
803
- def trace_cls(self, *args, **kwargs):
804
- """Apply class tracing decorator. Imports from decorators module."""
805
- from .decorators import trace_cls as _trace_cls
806
-
807
- return _trace_cls(logger=self, *args, **kwargs)
808
-
809
- def trace(self, *args, **kwargs):
810
- """Apply universal tracing decorator. Imports from decorators module."""
811
- from .decorators import trace as _trace
812
-
813
- return _trace(logger=self, *args, **kwargs)
814
-
815
- def animate_spinning(
816
- self,
817
- text: str,
818
- duration: Optional[float] = None,
819
- frames: Optional[List[str]] = None,
820
- speed: float = 0.1,
821
- level: LoggerLevelName = "info",
822
- ) -> None:
823
- """Display spinning animation with logging.
824
-
825
- Args:
826
- text: Text to display with spinner
827
- duration: Duration to run animation (defaults to 2.0)
828
- frames: Custom spinner frames
829
- speed: Speed of animation
830
- level: Log level to use
831
- """
832
- self.log(level, f"Starting: {text}")
833
- animate_spinning(
834
- text,
835
- duration=duration,
836
- frames=frames,
837
- speed=speed,
838
- )
839
- self.log(level, f"Completed: {text}")
840
-
841
- def add_file(
842
- self,
843
- file_config: Union[str, Path, FileConfig],
844
- level: Optional[Union[str, int]] = None,
845
- ) -> None:
846
- """Add a new file handler to the logger.
847
-
848
- Args:
849
- file_config: File configuration
850
- level: Optional level for this handler (uses logger level if None)
851
- """
852
- handler_level = level or self._logger.level
853
- if isinstance(handler_level, str):
854
- level_map = {
855
- "debug": _logging.DEBUG,
856
- "info": _logging.INFO,
857
- "warning": _logging.WARNING,
858
- "error": _logging.ERROR,
859
- "critical": _logging.CRITICAL,
860
- }
861
- handler_level = level_map.get(handler_level.lower(), _logging.WARNING)
862
-
863
- self._setup_file_handler(file_config, handler_level)
864
-
865
- def remove_handlers(self, handler_types: Optional[List[str]] = None) -> None:
866
- """Remove handlers from the logger.
867
-
868
- Args:
869
- handler_types: List of handler type names to remove.
870
- If None, removes all handlers.
871
- Options: ['file', 'console', 'rich', 'rotating']
872
- """
873
- if handler_types is None:
874
- self._logger.handlers.clear()
875
- return
876
-
877
- handlers_to_remove = []
878
- for handler in self._logger.handlers:
879
- handler_type = type(handler).__name__.lower()
880
-
881
- if any(ht in handler_type for ht in handler_types):
882
- handlers_to_remove.append(handler)
883
-
884
- for handler in handlers_to_remove:
885
- self._logger.removeHandler(handler)
886
-
887
- def get_file_paths(self) -> List[Path]:
888
- """Get all file paths being logged to."""
889
- file_paths = []
890
-
891
- for handler in self._logger.handlers:
892
- if hasattr(handler, "baseFilename"):
893
- file_paths.append(Path(handler.baseFilename))
894
-
895
- return file_paths
896
-
897
- def flush(self) -> None:
898
- """Flush all handlers."""
899
- for handler in self._logger.handlers:
900
- handler.flush()
901
-
902
- def close(self) -> None:
903
- """Close all handlers and cleanup resources."""
904
- for handler in self._logger.handlers[:]:
905
- handler.close()
906
- self._logger.removeHandler(handler)
907
-
908
-
909
- # -----------------------------------------------------------------------------
910
- # Factory
911
- # -----------------------------------------------------------------------------
912
-
913
-
914
- def create_logger_level(
915
- name: str,
916
- level: int,
917
- color: Optional[str] = None,
918
- style: Optional[str] = None,
919
- ) -> None:
920
- """
921
- Create a custom logging level.
922
-
923
- Args:
924
- name: The name of the logging level (e.g., "TRACE", "SUCCESS")
925
- level: The numeric level value (should be between existing levels)
926
- color: Optional color for rich formatting (e.g., "green", "blue")
927
- style: Optional style for rich formatting (e.g., "bold", "italic")
928
- """
929
- # Convert name to uppercase for consistency
930
- level_name = name.upper()
931
-
932
- # Add the level to the logging module
933
- _logging.addLevelName(level, level_name)
934
-
935
- # Create a method on the Logger class for this level
936
- def log_method(self, message, *args, **kwargs):
937
- if self.isEnabledFor(level):
938
- self._log(level, message, args, **kwargs)
939
-
940
- # Add the method to the standard logging.Logger class
941
- setattr(_logging.Logger, name.lower(), log_method)
942
-
943
- # Store level info for potential rich formatting
944
- if hasattr(_logging, "_custom_level_info"):
945
- _logging._custom_level_info[level] = {
946
- "name": level_name,
947
- "color": color,
948
- "style": style,
949
- }
950
- else:
951
- _logging._custom_level_info = {
952
- level: {"name": level_name, "color": color, "style": style}
953
- }
954
-
955
-
956
- def create_logger(
957
- name: Optional[str] = None,
958
- level: Optional[Union[LoggerLevelName, int]] = None,
959
- rich: bool = True,
960
- display_all: bool = False,
961
- levels: Optional[Dict[LoggerLevelName, LoggerLevelSettings]] = None,
962
- file: Optional[Union[str, Path, FileConfig]] = None,
963
- files: Optional[List[Union[str, Path, FileConfig]]] = None,
964
- format: Optional[str] = None,
965
- date_format: Optional[str] = None,
966
- json_logs: bool = False,
967
- console: bool = True,
968
- handlers: Optional[List[_logging.Handler]] = None,
969
- ) -> Logger:
970
- """
971
- Get a logger instance.
972
-
973
- Args:
974
- name: Name for the logger. If None, uses caller's function name
975
- level: Logging level. If None, defaults to "debug" if display_all else "warning"
976
- rich: Whether to use rich formatting for output
977
- display_all: If True, sets effective level to debug to show all messages
978
- levels: Custom level styles to override defaults
979
- file: Single file configuration for logging
980
- files: Multiple file configurations for logging
981
- format: Custom log format string
982
- date_format: Date format for timestamps
983
- json_logs: Whether to output structured JSON logs
984
- console: Whether to log to console (default True)
985
- handlers: Additional custom handlers to add
986
-
987
- Returns:
988
- A Logger instance with the specified configuration.
989
- """
990
- if name is None:
991
- frame = inspect.currentframe()
992
- if frame and frame.f_back:
993
- name = frame.f_back.f_code.co_name
994
- else:
995
- name = "logger"
996
-
997
- return Logger(
998
- name=name,
999
- level=level,
1000
- rich=rich,
1001
- display_all=display_all,
1002
- level_styles=levels,
1003
- file=file,
1004
- files=files,
1005
- format=format,
1006
- date_format=date_format,
1007
- json_logs=json_logs,
1008
- console=console,
1009
- handlers=handlers,
1010
- )
1011
-
1012
-
1013
- # internal logger and helper
1014
- _logger = Logger("hammad", level="warning")
1015
-
1016
-
1017
- def _get_internal_logger(name: str) -> Logger:
1018
- return Logger(name=name, level="warning")