hammad-python 0.0.14__py3-none-any.whl → 0.0.15__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 (101) hide show
  1. hammad_python-0.0.15.dist-info/METADATA +184 -0
  2. hammad_python-0.0.15.dist-info/RECORD +4 -0
  3. hammad/__init__.py +0 -1
  4. hammad/ai/__init__.py +0 -1
  5. hammad/ai/_utils.py +0 -142
  6. hammad/ai/completions/__init__.py +0 -45
  7. hammad/ai/completions/client.py +0 -684
  8. hammad/ai/completions/create.py +0 -710
  9. hammad/ai/completions/settings.py +0 -100
  10. hammad/ai/completions/types.py +0 -792
  11. hammad/ai/completions/utils.py +0 -486
  12. hammad/ai/embeddings/__init__.py +0 -35
  13. hammad/ai/embeddings/client/__init__.py +0 -1
  14. hammad/ai/embeddings/client/base_embeddings_client.py +0 -26
  15. hammad/ai/embeddings/client/fastembed_text_embeddings_client.py +0 -200
  16. hammad/ai/embeddings/client/litellm_embeddings_client.py +0 -288
  17. hammad/ai/embeddings/create.py +0 -159
  18. hammad/ai/embeddings/types.py +0 -69
  19. hammad/cache/__init__.py +0 -40
  20. hammad/cache/base_cache.py +0 -181
  21. hammad/cache/cache.py +0 -169
  22. hammad/cache/decorators.py +0 -261
  23. hammad/cache/file_cache.py +0 -80
  24. hammad/cache/ttl_cache.py +0 -74
  25. hammad/cli/__init__.py +0 -33
  26. hammad/cli/animations.py +0 -573
  27. hammad/cli/plugins.py +0 -781
  28. hammad/cli/styles/__init__.py +0 -55
  29. hammad/cli/styles/settings.py +0 -139
  30. hammad/cli/styles/types.py +0 -358
  31. hammad/cli/styles/utils.py +0 -480
  32. hammad/data/__init__.py +0 -56
  33. hammad/data/collections/__init__.py +0 -34
  34. hammad/data/collections/base_collection.py +0 -58
  35. hammad/data/collections/collection.py +0 -452
  36. hammad/data/collections/searchable_collection.py +0 -556
  37. hammad/data/collections/vector_collection.py +0 -596
  38. hammad/data/configurations/__init__.py +0 -35
  39. hammad/data/configurations/configuration.py +0 -564
  40. hammad/data/databases/__init__.py +0 -21
  41. hammad/data/databases/database.py +0 -902
  42. hammad/data/models/__init__.py +0 -44
  43. hammad/data/models/base/__init__.py +0 -35
  44. hammad/data/models/base/fields.py +0 -546
  45. hammad/data/models/base/model.py +0 -1078
  46. hammad/data/models/base/utils.py +0 -280
  47. hammad/data/models/pydantic/__init__.py +0 -55
  48. hammad/data/models/pydantic/converters.py +0 -632
  49. hammad/data/models/pydantic/models/__init__.py +0 -28
  50. hammad/data/models/pydantic/models/arbitrary_model.py +0 -46
  51. hammad/data/models/pydantic/models/cacheable_model.py +0 -79
  52. hammad/data/models/pydantic/models/fast_model.py +0 -318
  53. hammad/data/models/pydantic/models/function_model.py +0 -176
  54. hammad/data/models/pydantic/models/subscriptable_model.py +0 -63
  55. hammad/data/types/__init__.py +0 -41
  56. hammad/data/types/file.py +0 -358
  57. hammad/data/types/multimodal/__init__.py +0 -24
  58. hammad/data/types/multimodal/audio.py +0 -96
  59. hammad/data/types/multimodal/image.py +0 -80
  60. hammad/data/types/text.py +0 -1066
  61. hammad/formatting/__init__.py +0 -38
  62. hammad/formatting/json/__init__.py +0 -21
  63. hammad/formatting/json/converters.py +0 -152
  64. hammad/formatting/text/__init__.py +0 -63
  65. hammad/formatting/text/converters.py +0 -723
  66. hammad/formatting/text/markdown.py +0 -131
  67. hammad/formatting/yaml/__init__.py +0 -26
  68. hammad/formatting/yaml/converters.py +0 -5
  69. hammad/logging/__init__.py +0 -35
  70. hammad/logging/decorators.py +0 -834
  71. hammad/logging/logger.py +0 -954
  72. hammad/mcp/__init__.py +0 -50
  73. hammad/mcp/client/__init__.py +0 -1
  74. hammad/mcp/client/client.py +0 -523
  75. hammad/mcp/client/client_service.py +0 -393
  76. hammad/mcp/client/settings.py +0 -178
  77. hammad/mcp/servers/__init__.py +0 -1
  78. hammad/mcp/servers/launcher.py +0 -1161
  79. hammad/performance/__init__.py +0 -36
  80. hammad/performance/imports.py +0 -231
  81. hammad/performance/runtime/__init__.py +0 -32
  82. hammad/performance/runtime/decorators.py +0 -142
  83. hammad/performance/runtime/run.py +0 -299
  84. hammad/py.typed +0 -0
  85. hammad/service/__init__.py +0 -49
  86. hammad/service/create.py +0 -532
  87. hammad/service/decorators.py +0 -285
  88. hammad/typing/__init__.py +0 -407
  89. hammad/web/__init__.py +0 -43
  90. hammad/web/http/__init__.py +0 -1
  91. hammad/web/http/client.py +0 -944
  92. hammad/web/models.py +0 -245
  93. hammad/web/openapi/__init__.py +0 -1
  94. hammad/web/openapi/client.py +0 -740
  95. hammad/web/search/__init__.py +0 -1
  96. hammad/web/search/client.py +0 -988
  97. hammad/web/utils.py +0 -472
  98. hammad_python-0.0.14.dist-info/METADATA +0 -70
  99. hammad_python-0.0.14.dist-info/RECORD +0 -99
  100. {hammad_python-0.0.14.dist-info → hammad_python-0.0.15.dist-info}/WHEEL +0 -0
  101. {hammad_python-0.0.14.dist-info → hammad_python-0.0.15.dist-info}/licenses/LICENSE +0 -0
hammad/logging/logger.py DELETED
@@ -1,954 +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
- return self._style._fmt.format(**record.__dict__)
245
-
246
- def _build_renderable_style_string(self, style_dict: dict) -> str:
247
- """Build a rich markup style string from a CLIStyleRenderableSettings dictionary."""
248
- style_parts = []
249
-
250
- # Handle all the style attributes from CLIStyleRenderableSettings
251
- for attr in [
252
- "bold",
253
- "italic",
254
- "dim",
255
- "underline",
256
- "strike",
257
- "blink",
258
- "blink2",
259
- "reverse",
260
- "conceal",
261
- "underline2",
262
- "frame",
263
- "encircle",
264
- "overline",
265
- ]:
266
- if style_dict.get(attr):
267
- style_parts.append(attr)
268
-
269
- return " ".join(style_parts) if style_parts else ""
270
-
271
-
272
- # -----------------------------------------------------------------------------
273
- # Logger
274
- # -----------------------------------------------------------------------------
275
-
276
-
277
- @dataclass
278
- class Logger:
279
- """Flexible logger with rich styling and custom level support."""
280
-
281
- _logger: _logging.Logger = field(init=False)
282
- """The underlying logging.Logger instance."""
283
-
284
- _level_styles: Dict[str, LoggerLevelSettings] = field(init=False)
285
- """Custom level styles."""
286
-
287
- _custom_levels: Dict[str, int] = field(init=False)
288
- """Custom logging levels."""
289
-
290
- _user_level: str = field(init=False)
291
- """User-specified logging level."""
292
-
293
- def __init__(
294
- self,
295
- name: Optional[str] = None,
296
- level: Optional[Union[str, int]] = None,
297
- rich: bool = True,
298
- display_all: bool = False,
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,
307
- ) -> None:
308
- """
309
- Initialize a new Logger instance.
310
-
311
- Args:
312
- name: Name for the logger. If None, defaults to "hammad"
313
- level: Logging level. If None, defaults to "debug" if display_all else "warning"
314
- rich: Whether to use rich formatting for output
315
- display_all: If True, sets effective level to debug to show all messages
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
324
- """
325
- logger_name = name or "hammad"
326
-
327
- # Initialize custom levels dict
328
- self._custom_levels = {}
329
-
330
- # Initialize level styles with defaults
331
- self._level_styles = DEFAULT_LEVEL_STYLES.copy()
332
- if level_styles:
333
- self._level_styles.update(level_styles)
334
-
335
- # Handle integer levels by converting to string names
336
- if isinstance(level, int):
337
- # Map standard logging levels to their names
338
- int_to_name = {
339
- _logging.DEBUG: "debug",
340
- _logging.INFO: "info",
341
- _logging.WARNING: "warning",
342
- _logging.ERROR: "error",
343
- _logging.CRITICAL: "critical",
344
- }
345
- level = int_to_name.get(level, "warning")
346
-
347
- self._user_level = level or "warning"
348
-
349
- if display_all:
350
- effective_level = "debug"
351
- else:
352
- effective_level = self._user_level
353
-
354
- # Standard level mapping
355
- level_map = {
356
- "debug": _logging.DEBUG,
357
- "info": _logging.INFO,
358
- "warning": _logging.WARNING,
359
- "error": _logging.ERROR,
360
- "critical": _logging.CRITICAL,
361
- }
362
-
363
- # Check if it's a custom level
364
- if effective_level.lower() in self._custom_levels:
365
- log_level = self._custom_levels[effective_level.lower()]
366
- else:
367
- log_level = level_map.get(effective_level.lower(), _logging.WARNING)
368
-
369
- # Create logger
370
- self._logger = _logging.getLogger(logger_name)
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
-
381
- # Clear any existing handlers
382
- if self._logger.hasHandlers():
383
- self._logger.handlers.clear()
384
-
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)
392
-
393
- self._logger.setLevel(log_level)
394
- self._logger.propagate = False
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
-
412
- def _setup_rich_handler(self, log_level: int) -> None:
413
- """Setup rich handler for the logger."""
414
- console = get_rich_console()
415
-
416
- handler = RichHandler(
417
- level=log_level,
418
- console=console,
419
- rich_tracebacks=True,
420
- show_time=self._date_format is not None,
421
- show_path=False,
422
- markup=True,
423
- )
424
-
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
-
431
- handler.setFormatter(formatter)
432
-
433
- # Add our custom filter
434
- handler.addFilter(RichLoggerFilter(self._level_styles))
435
-
436
- self._logger.addHandler(handler)
437
-
438
- def _setup_standard_handler(self, log_level: int) -> None:
439
- """Setup standard handler for the logger."""
440
- handler = _logging.StreamHandler()
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
-
505
- handler.setFormatter(formatter)
506
- handler.setLevel(log_level)
507
-
508
- self._logger.addHandler(handler)
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
-
536
- def add_level(
537
- self, name: str, value: int, style: Optional[LoggerLevelSettings] = None
538
- ) -> None:
539
- """
540
- Add a custom logging level.
541
-
542
- Args:
543
- name: Name of the custom level
544
- value: Numeric value for the level (should be unique)
545
- style: Optional style settings for the level
546
- """
547
- # Add to Python's logging module
548
- _logging.addLevelName(value, name.upper())
549
-
550
- # Store in our custom levels
551
- self._custom_levels[name.lower()] = value
552
-
553
- # Add style if provided
554
- if style:
555
- self._level_styles[name.lower()] = style
556
-
557
- # Update filters if using rich handler
558
- for handler in self._logger.handlers:
559
- if isinstance(handler, RichHandler):
560
- # Remove old filter and add new one with updated styles
561
- for f in handler.filters[:]:
562
- if isinstance(f, RichLoggerFilter):
563
- handler.removeFilter(f)
564
- handler.addFilter(RichLoggerFilter(self._level_styles))
565
-
566
- @property
567
- def level(self) -> str:
568
- """Get the current logging level."""
569
- return self._user_level
570
-
571
- @level.setter
572
- def level(self, value: str) -> None:
573
- """Set the logging level."""
574
- self._user_level = value
575
-
576
- # Standard level mapping
577
- level_map = {
578
- "debug": _logging.DEBUG,
579
- "info": _logging.INFO,
580
- "warning": _logging.WARNING,
581
- "error": _logging.ERROR,
582
- "critical": _logging.CRITICAL,
583
- }
584
-
585
- # Check custom levels
586
- if value.lower() in self._custom_levels:
587
- log_level = self._custom_levels[value.lower()]
588
- else:
589
- log_level = level_map.get(value.lower(), _logging.WARNING)
590
-
591
- # Update logger level
592
- self._logger.setLevel(log_level)
593
-
594
- # Update handler levels
595
- for handler in self._logger.handlers:
596
- handler.setLevel(log_level)
597
-
598
- # Convenience methods for standard logging levels
599
- def debug(self, message: str, *args: Any, **kwargs: Any) -> None:
600
- """Log a debug message."""
601
- self._logger.debug(message, *args, **kwargs)
602
-
603
- def info(self, message: str, *args: Any, **kwargs: Any) -> None:
604
- """Log an info message."""
605
- self._logger.info(message, *args, **kwargs)
606
-
607
- def warning(self, message: str, *args: Any, **kwargs: Any) -> None:
608
- """Log a warning message."""
609
- self._logger.warning(message, *args, **kwargs)
610
-
611
- def error(self, message: str, *args: Any, **kwargs: Any) -> None:
612
- """Log an error message."""
613
- self._logger.error(message, *args, **kwargs)
614
-
615
- def critical(self, message: str, *args: Any, **kwargs: Any) -> None:
616
- """Log a critical message."""
617
- self._logger.critical(message, *args, **kwargs)
618
-
619
- def log(
620
- self, level: Union[str, int], message: str, *args: Any, **kwargs: Any
621
- ) -> None:
622
- """
623
- Log a message at the specified level.
624
-
625
- Args:
626
- level: The level to log at (can be standard or custom)
627
- message: The message to log
628
- *args: Additional positional arguments for the logger
629
- **kwargs: Additional keyword arguments for the logger
630
- """
631
- # Standard level mapping
632
- level_map = {
633
- "debug": _logging.DEBUG,
634
- "info": _logging.INFO,
635
- "warning": _logging.WARNING,
636
- "error": _logging.ERROR,
637
- "critical": _logging.CRITICAL,
638
- }
639
-
640
- # Handle integer levels
641
- if isinstance(level, int):
642
- # Use the integer level directly
643
- log_level = level
644
- else:
645
- # Check custom levels first
646
- if level.lower() in self._custom_levels:
647
- log_level = self._custom_levels[level.lower()]
648
- else:
649
- log_level = level_map.get(level.lower(), _logging.WARNING)
650
-
651
- self._logger.log(log_level, message, *args, **kwargs)
652
-
653
- @property
654
- def name(self) -> str:
655
- """Get the logger name."""
656
- return self._logger.name
657
-
658
- @property
659
- def handlers(self) -> list[_logging.Handler]:
660
- """Get the logger handlers."""
661
- return self._logger.handlers
662
-
663
- def get_logger(self) -> _logging.Logger:
664
- """Get the underlying logging.Logger instance."""
665
- return self._logger
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
-
852
-
853
- # -----------------------------------------------------------------------------
854
- # Factory
855
- # -----------------------------------------------------------------------------
856
-
857
-
858
- def create_logger_level(
859
- name: str,
860
- level: int,
861
- color: Optional[str] = None,
862
- style: Optional[str] = None,
863
- ) -> None:
864
- """
865
- Create a custom logging level.
866
-
867
- Args:
868
- name: The name of the logging level (e.g., "TRACE", "SUCCESS")
869
- level: The numeric level value (should be between existing levels)
870
- color: Optional color for rich formatting (e.g., "green", "blue")
871
- style: Optional style for rich formatting (e.g., "bold", "italic")
872
- """
873
- # Convert name to uppercase for consistency
874
- level_name = name.upper()
875
-
876
- # Add the level to the logging module
877
- _logging.addLevelName(level, level_name)
878
-
879
- # Create a method on the Logger class for this level
880
- def log_method(self, message, *args, **kwargs):
881
- if self.isEnabledFor(level):
882
- self._log(level, message, args, **kwargs)
883
-
884
- # Add the method to the standard logging.Logger class
885
- setattr(_logging.Logger, name.lower(), log_method)
886
-
887
- # Store level info for potential rich formatting
888
- if hasattr(_logging, "_custom_level_info"):
889
- _logging._custom_level_info[level] = {
890
- "name": level_name,
891
- "color": color,
892
- "style": style,
893
- }
894
- else:
895
- _logging._custom_level_info = {
896
- level: {"name": level_name, "color": color, "style": style}
897
- }
898
-
899
-
900
- def create_logger(
901
- name: Optional[str] = None,
902
- level: Optional[Union[str, int]] = None,
903
- rich: bool = True,
904
- display_all: bool = False,
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,
913
- ) -> Logger:
914
- """
915
- Get a logger instance.
916
-
917
- Args:
918
- name: Name for the logger. If None, uses caller's function name
919
- level: Logging level. If None, defaults to "debug" if display_all else "warning"
920
- rich: Whether to use rich formatting for output
921
- display_all: If True, sets effective level to debug to show all messages
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
930
-
931
- Returns:
932
- A Logger instance with the specified configuration.
933
- """
934
- if name is None:
935
- frame = inspect.currentframe()
936
- if frame and frame.f_back:
937
- name = frame.f_back.f_code.co_name
938
- else:
939
- name = "logger"
940
-
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
- )