chuk-tool-processor 0.6.12__py3-none-any.whl → 0.6.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.

Potentially problematic release.


This version of chuk-tool-processor might be problematic. Click here for more details.

Files changed (56) hide show
  1. chuk_tool_processor/core/__init__.py +1 -1
  2. chuk_tool_processor/core/exceptions.py +10 -4
  3. chuk_tool_processor/core/processor.py +97 -97
  4. chuk_tool_processor/execution/strategies/inprocess_strategy.py +142 -150
  5. chuk_tool_processor/execution/strategies/subprocess_strategy.py +200 -205
  6. chuk_tool_processor/execution/tool_executor.py +82 -84
  7. chuk_tool_processor/execution/wrappers/caching.py +102 -103
  8. chuk_tool_processor/execution/wrappers/rate_limiting.py +45 -42
  9. chuk_tool_processor/execution/wrappers/retry.py +23 -25
  10. chuk_tool_processor/logging/__init__.py +23 -17
  11. chuk_tool_processor/logging/context.py +40 -45
  12. chuk_tool_processor/logging/formatter.py +22 -21
  13. chuk_tool_processor/logging/helpers.py +24 -38
  14. chuk_tool_processor/logging/metrics.py +11 -13
  15. chuk_tool_processor/mcp/__init__.py +8 -12
  16. chuk_tool_processor/mcp/mcp_tool.py +124 -112
  17. chuk_tool_processor/mcp/register_mcp_tools.py +17 -17
  18. chuk_tool_processor/mcp/setup_mcp_http_streamable.py +11 -13
  19. chuk_tool_processor/mcp/setup_mcp_sse.py +11 -13
  20. chuk_tool_processor/mcp/setup_mcp_stdio.py +7 -9
  21. chuk_tool_processor/mcp/stream_manager.py +168 -204
  22. chuk_tool_processor/mcp/transport/__init__.py +4 -4
  23. chuk_tool_processor/mcp/transport/base_transport.py +43 -58
  24. chuk_tool_processor/mcp/transport/http_streamable_transport.py +145 -163
  25. chuk_tool_processor/mcp/transport/sse_transport.py +217 -255
  26. chuk_tool_processor/mcp/transport/stdio_transport.py +171 -189
  27. chuk_tool_processor/models/__init__.py +1 -1
  28. chuk_tool_processor/models/execution_strategy.py +16 -21
  29. chuk_tool_processor/models/streaming_tool.py +28 -25
  30. chuk_tool_processor/models/tool_call.py +19 -34
  31. chuk_tool_processor/models/tool_export_mixin.py +22 -8
  32. chuk_tool_processor/models/tool_result.py +40 -77
  33. chuk_tool_processor/models/validated_tool.py +14 -16
  34. chuk_tool_processor/plugins/__init__.py +1 -1
  35. chuk_tool_processor/plugins/discovery.py +10 -10
  36. chuk_tool_processor/plugins/parsers/__init__.py +1 -1
  37. chuk_tool_processor/plugins/parsers/base.py +1 -2
  38. chuk_tool_processor/plugins/parsers/function_call_tool.py +13 -8
  39. chuk_tool_processor/plugins/parsers/json_tool.py +4 -3
  40. chuk_tool_processor/plugins/parsers/openai_tool.py +12 -7
  41. chuk_tool_processor/plugins/parsers/xml_tool.py +4 -4
  42. chuk_tool_processor/registry/__init__.py +12 -12
  43. chuk_tool_processor/registry/auto_register.py +22 -30
  44. chuk_tool_processor/registry/decorators.py +127 -129
  45. chuk_tool_processor/registry/interface.py +26 -23
  46. chuk_tool_processor/registry/metadata.py +27 -22
  47. chuk_tool_processor/registry/provider.py +17 -18
  48. chuk_tool_processor/registry/providers/__init__.py +16 -19
  49. chuk_tool_processor/registry/providers/memory.py +18 -25
  50. chuk_tool_processor/registry/tool_export.py +42 -51
  51. chuk_tool_processor/utils/validation.py +15 -16
  52. {chuk_tool_processor-0.6.12.dist-info → chuk_tool_processor-0.6.13.dist-info}/METADATA +1 -1
  53. chuk_tool_processor-0.6.13.dist-info/RECORD +60 -0
  54. chuk_tool_processor-0.6.12.dist-info/RECORD +0 -60
  55. {chuk_tool_processor-0.6.12.dist-info → chuk_tool_processor-0.6.13.dist-info}/WHEEL +0 -0
  56. {chuk_tool_processor-0.6.12.dist-info → chuk_tool_processor-0.6.13.dist-info}/top_level.txt +0 -0
@@ -7,44 +7,46 @@ Two layers of limits are enforced:
7
7
  * **Global** - ``<N requests> / <period>`` over *all* tools.
8
8
  * **Per-tool** - independent ``<N requests> / <period>`` windows.
9
9
 
10
- A simple sliding-window algorithm with timestamp queues is used.
10
+ A simple sliding-window algorithm with timestamp queues is used.
11
11
  `asyncio.Lock` guards shared state so the wrapper can be used safely from
12
12
  multiple coroutines.
13
13
  """
14
+
14
15
  from __future__ import annotations
15
16
 
16
17
  import asyncio
17
18
  import inspect
18
19
  import time
19
- from typing import Any, Dict, List, Optional, Tuple, Union
20
+ from typing import Any
20
21
 
22
+ from chuk_tool_processor.logging import get_logger
21
23
  from chuk_tool_processor.models.tool_call import ToolCall
22
24
  from chuk_tool_processor.models.tool_result import ToolResult
23
- from chuk_tool_processor.logging import get_logger
24
25
 
25
26
  logger = get_logger("chuk_tool_processor.execution.wrappers.rate_limiting")
26
27
 
28
+
27
29
  # --------------------------------------------------------------------------- #
28
30
  # Core limiter
29
31
  # --------------------------------------------------------------------------- #
30
32
  class RateLimiter:
31
33
  """
32
34
  Async-native rate limiter for controlling execution frequency.
33
-
35
+
34
36
  Implements a sliding window algorithm to enforce rate limits both globally
35
37
  and per-tool. All operations are thread-safe using asyncio locks.
36
38
  """
37
-
39
+
38
40
  def __init__(
39
41
  self,
40
42
  *,
41
- global_limit: Optional[int] = None,
43
+ global_limit: int | None = None,
42
44
  global_period: float = 60.0,
43
- tool_limits: Optional[Dict[str, Tuple[int, float]]] = None,
45
+ tool_limits: dict[str, tuple[int, float]] | None = None,
44
46
  ) -> None:
45
47
  """
46
48
  Initialize the rate limiter.
47
-
49
+
48
50
  Args:
49
51
  global_limit: Maximum global requests per period (None = no limit)
50
52
  global_period: Time period in seconds for the global limit
@@ -55,13 +57,13 @@ class RateLimiter:
55
57
  self.tool_limits = tool_limits or {}
56
58
 
57
59
  # Timestamp queues
58
- self._global_ts: List[float] = []
59
- self._tool_ts: Dict[str, List[float]] = {}
60
+ self._global_ts: list[float] = []
61
+ self._tool_ts: dict[str, list[float]] = {}
60
62
 
61
63
  # Locks for thread safety
62
64
  self._global_lock = asyncio.Lock()
63
- self._tool_locks: Dict[str, asyncio.Lock] = {}
64
-
65
+ self._tool_locks: dict[str, asyncio.Lock] = {}
66
+
65
67
  logger.debug(
66
68
  f"Initialized rate limiter: global={global_limit}/{global_period}s, "
67
69
  f"tool-specific={len(self.tool_limits)} tools"
@@ -77,7 +79,7 @@ class RateLimiter:
77
79
  async with self._global_lock:
78
80
  now = time.monotonic()
79
81
  cutoff = now - self.global_period
80
-
82
+
81
83
  # Prune expired timestamps
82
84
  self._global_ts = [t for t in self._global_ts if t > cutoff]
83
85
 
@@ -88,7 +90,7 @@ class RateLimiter:
88
90
 
89
91
  # Calculate wait time until a slot becomes available
90
92
  wait = (self._global_ts[0] + self.global_period) - now
91
-
93
+
92
94
  logger.debug(f"Global rate limit reached, waiting {wait:.2f}s")
93
95
  await asyncio.sleep(wait)
94
96
 
@@ -105,7 +107,7 @@ class RateLimiter:
105
107
  async with lock:
106
108
  now = time.monotonic()
107
109
  cutoff = now - period
108
-
110
+
109
111
  # Prune expired timestamps in-place
110
112
  buf[:] = [t for t in buf if t > cutoff]
111
113
 
@@ -116,7 +118,7 @@ class RateLimiter:
116
118
 
117
119
  # Calculate wait time until a slot becomes available
118
120
  wait = (buf[0] + period) - now
119
-
121
+
120
122
  logger.debug(f"Tool '{tool}' rate limit reached, waiting {wait:.2f}s")
121
123
  await asyncio.sleep(wait)
122
124
 
@@ -124,32 +126,32 @@ class RateLimiter:
124
126
  async def wait(self, tool: str) -> None:
125
127
  """
126
128
  Block until rate limits allow execution.
127
-
129
+
128
130
  This method blocks until both global and tool-specific rate limits
129
131
  allow one more execution of the specified tool.
130
-
132
+
131
133
  Args:
132
134
  tool: Name of the tool being executed
133
135
  """
134
136
  await self._acquire_global()
135
137
  await self._acquire_tool(tool)
136
-
137
- async def check_limits(self, tool: str) -> Tuple[bool, bool]:
138
+
139
+ async def check_limits(self, tool: str) -> tuple[bool, bool]:
138
140
  """
139
141
  Check if the tool would be rate limited without consuming a slot.
140
-
142
+
141
143
  This is a non-blocking method useful for checking limits without
142
144
  affecting the rate limiting state.
143
-
145
+
144
146
  Args:
145
147
  tool: Name of the tool to check
146
-
148
+
147
149
  Returns:
148
150
  Tuple of (global_limit_reached, tool_limit_reached)
149
151
  """
150
152
  global_limited = False
151
153
  tool_limited = False
152
-
154
+
153
155
  # Check global limit
154
156
  if self.global_limit is not None:
155
157
  async with self._global_lock:
@@ -157,7 +159,7 @@ class RateLimiter:
157
159
  cutoff = now - self.global_period
158
160
  active_ts = [t for t in self._global_ts if t > cutoff]
159
161
  global_limited = len(active_ts) >= self.global_limit
160
-
162
+
161
163
  # Check tool limit
162
164
  if tool in self.tool_limits:
163
165
  limit, period = self.tool_limits[tool]
@@ -167,7 +169,7 @@ class RateLimiter:
167
169
  buf = self._tool_ts.get(tool, [])
168
170
  active_ts = [t for t in buf if t > cutoff]
169
171
  tool_limited = len(active_ts) >= limit
170
-
172
+
171
173
  return global_limited, tool_limited
172
174
 
173
175
 
@@ -177,7 +179,7 @@ class RateLimiter:
177
179
  class RateLimitedToolExecutor:
178
180
  """
179
181
  Executor wrapper that applies rate limiting to tool executions.
180
-
182
+
181
183
  This wrapper delegates to another executor but ensures that all
182
184
  tool calls respect the configured rate limits.
183
185
  """
@@ -185,48 +187,48 @@ class RateLimitedToolExecutor:
185
187
  def __init__(self, executor: Any, limiter: RateLimiter) -> None:
186
188
  """
187
189
  Initialize the rate-limited executor.
188
-
190
+
189
191
  Args:
190
192
  executor: The underlying executor to wrap
191
193
  limiter: The RateLimiter that controls execution frequency
192
194
  """
193
195
  self.executor = executor
194
196
  self.limiter = limiter
195
- logger.debug(f"Initialized rate-limited executor")
197
+ logger.debug("Initialized rate-limited executor")
196
198
 
197
199
  async def execute(
198
200
  self,
199
- calls: List[ToolCall],
200
- timeout: Optional[float] = None,
201
+ calls: list[ToolCall],
202
+ timeout: float | None = None,
201
203
  use_cache: bool = True,
202
- ) -> List[ToolResult]:
204
+ ) -> list[ToolResult]:
203
205
  """
204
206
  Execute tool calls while respecting rate limits.
205
-
207
+
206
208
  This method blocks until rate limits allow execution, then delegates
207
209
  to the underlying executor.
208
-
210
+
209
211
  Args:
210
212
  calls: List of tool calls to execute
211
213
  timeout: Optional timeout for execution
212
214
  use_cache: Whether to use cached results (forwarded to underlying executor)
213
-
215
+
214
216
  Returns:
215
217
  List of tool results
216
218
  """
217
219
  if not calls:
218
220
  return []
219
-
221
+
220
222
  # Block for each call *before* dispatching to the wrapped executor
221
223
  for c in calls:
222
224
  await self.limiter.wait(c.tool)
223
-
225
+
224
226
  # Check if the executor has a use_cache parameter
225
227
  if hasattr(self.executor, "execute"):
226
228
  sig = inspect.signature(self.executor.execute)
227
229
  if "use_cache" in sig.parameters:
228
230
  return await self.executor.execute(calls, timeout=timeout, use_cache=use_cache)
229
-
231
+
230
232
  # Fall back to standard execute method
231
233
  return await self.executor.execute(calls, timeout=timeout)
232
234
 
@@ -237,7 +239,7 @@ class RateLimitedToolExecutor:
237
239
  def rate_limited(limit: int, period: float = 60.0):
238
240
  """
239
241
  Class decorator that marks a Tool with default rate-limit metadata.
240
-
242
+
241
243
  This allows higher-level code to detect and configure rate limiting
242
244
  for the tool class.
243
245
 
@@ -246,17 +248,18 @@ def rate_limited(limit: int, period: float = 60.0):
246
248
  class WeatherTool:
247
249
  async def execute(self, location: str) -> Dict[str, Any]:
248
250
  # Implementation
249
-
251
+
250
252
  Args:
251
253
  limit: Maximum number of calls allowed in the period
252
254
  period: Time period in seconds
253
-
255
+
254
256
  Returns:
255
257
  Decorated class with rate limit metadata
256
258
  """
259
+
257
260
  def decorator(cls):
258
261
  cls._rate_limit = limit
259
262
  cls._rate_period = period
260
263
  return cls
261
264
 
262
- return decorator
265
+ return decorator
@@ -6,13 +6,14 @@ Adds exponential-back-off retry logic and *deadline-aware* timeout handling so a
6
6
  `timeout=` passed by callers is treated as the **total wall-clock budget** for
7
7
  all attempts of a single tool call.
8
8
  """
9
+
9
10
  from __future__ import annotations
10
11
 
11
12
  import asyncio
12
13
  import random
13
14
  import time
14
- from datetime import datetime, timezone
15
- from typing import Any, Dict, List, Optional, Type
15
+ from datetime import UTC, datetime
16
+ from typing import Any
16
17
 
17
18
  from chuk_tool_processor.logging import get_logger
18
19
  from chuk_tool_processor.models.tool_call import ToolCall
@@ -33,8 +34,8 @@ class RetryConfig:
33
34
  base_delay: float = 1.0,
34
35
  max_delay: float = 60.0,
35
36
  jitter: bool = True,
36
- retry_on_exceptions: Optional[List[Type[Exception]]] = None,
37
- retry_on_error_substrings: Optional[List[str]] = None,
37
+ retry_on_exceptions: list[type[Exception]] | None = None,
38
+ retry_on_error_substrings: list[str] | None = None,
38
39
  ):
39
40
  if max_retries < 0:
40
41
  raise ValueError("max_retries cannot be negative")
@@ -52,8 +53,8 @@ class RetryConfig:
52
53
  self,
53
54
  attempt: int,
54
55
  *,
55
- error: Optional[Exception] = None,
56
- error_str: Optional[str] = None,
56
+ error: Exception | None = None,
57
+ error_str: str | None = None,
57
58
  ) -> bool:
58
59
  """Return *True* iff another retry is allowed for this attempt."""
59
60
  if attempt >= self.max_retries:
@@ -66,17 +67,14 @@ class RetryConfig:
66
67
  if error is not None and any(isinstance(error, exc) for exc in self.retry_on_exceptions):
67
68
  return True
68
69
 
69
- if error_str and any(substr in error_str for substr in self.retry_on_error_substrings):
70
- return True
71
-
72
- return False
70
+ return bool(error_str and any(substr in error_str for substr in self.retry_on_error_substrings))
73
71
 
74
72
  # --------------------------------------------------------------------- #
75
73
  # Back-off
76
74
  # --------------------------------------------------------------------- #
77
75
  def get_delay(self, attempt: int) -> float:
78
76
  """Exponential back-off delay for *attempt* (0-based)."""
79
- delay = min(self.base_delay * (2 ** attempt), self.max_delay)
77
+ delay = min(self.base_delay * (2**attempt), self.max_delay)
80
78
  if self.jitter:
81
79
  delay *= 0.5 + random.random() # jitter in [0.5, 1.5)
82
80
  return delay
@@ -94,8 +92,8 @@ class RetryableToolExecutor:
94
92
  self,
95
93
  executor: Any,
96
94
  *,
97
- default_config: Optional[RetryConfig] = None,
98
- tool_configs: Optional[Dict[str, RetryConfig]] = None,
95
+ default_config: RetryConfig | None = None,
96
+ tool_configs: dict[str, RetryConfig] | None = None,
99
97
  ):
100
98
  self.executor = executor
101
99
  self.default_config = default_config or RetryConfig()
@@ -109,15 +107,15 @@ class RetryableToolExecutor:
109
107
 
110
108
  async def execute(
111
109
  self,
112
- calls: List[ToolCall],
110
+ calls: list[ToolCall],
113
111
  *,
114
- timeout: Optional[float] = None,
112
+ timeout: float | None = None,
115
113
  use_cache: bool = True,
116
- ) -> List[ToolResult]:
114
+ ) -> list[ToolResult]:
117
115
  if not calls:
118
116
  return []
119
117
 
120
- out: List[ToolResult] = []
118
+ out: list[ToolResult] = []
121
119
  for call in calls:
122
120
  cfg = self._config_for(call.tool)
123
121
  out.append(await self._execute_single(call, cfg, timeout, use_cache))
@@ -130,11 +128,11 @@ class RetryableToolExecutor:
130
128
  self,
131
129
  call: ToolCall,
132
130
  cfg: RetryConfig,
133
- timeout: Optional[float],
131
+ timeout: float | None,
134
132
  use_cache: bool,
135
133
  ) -> ToolResult:
136
134
  attempt = 0
137
- last_error: Optional[str] = None
135
+ last_error: str | None = None
138
136
  pid = 0
139
137
  machine = "unknown"
140
138
 
@@ -156,8 +154,8 @@ class RetryableToolExecutor:
156
154
  tool=call.tool,
157
155
  result=None,
158
156
  error=f"Timeout after {timeout}s",
159
- start_time=datetime.now(timezone.utc),
160
- end_time=datetime.now(timezone.utc),
157
+ start_time=datetime.now(UTC),
158
+ end_time=datetime.now(UTC),
161
159
  machine=machine,
162
160
  pid=pid,
163
161
  attempts=attempt,
@@ -168,7 +166,7 @@ class RetryableToolExecutor:
168
166
  # ---------------------------------------------------------------- #
169
167
  # Execute one attempt
170
168
  # ---------------------------------------------------------------- #
171
- start_time = datetime.now(timezone.utc)
169
+ start_time = datetime.now(UTC)
172
170
  try:
173
171
  kwargs = {"timeout": remaining} if remaining is not None else {}
174
172
  if hasattr(self.executor, "use_cache"):
@@ -215,7 +213,7 @@ class RetryableToolExecutor:
215
213
  attempt += 1
216
214
  continue
217
215
 
218
- end_time = datetime.now(timezone.utc)
216
+ end_time = datetime.now(UTC)
219
217
  return ToolResult(
220
218
  tool=call.tool,
221
219
  result=None,
@@ -246,8 +244,8 @@ def retryable(
246
244
  base_delay: float = 1.0,
247
245
  max_delay: float = 60.0,
248
246
  jitter: bool = True,
249
- retry_on_exceptions: Optional[List[Type[Exception]]] = None,
250
- retry_on_error_substrings: Optional[List[str]] = None,
247
+ retry_on_exceptions: list[type[Exception]] | None = None,
248
+ retry_on_error_substrings: list[str] | None = None,
251
249
  ):
252
250
  """
253
251
  Class decorator that attaches a :class:`RetryConfig` to a *tool* class.
@@ -11,40 +11,44 @@ Key components:
11
11
  - Metrics collection for tools and parsers
12
12
  - Async-friendly context managers for spans and requests
13
13
  """
14
+
14
15
  from __future__ import annotations
15
16
 
16
17
  import logging
17
18
  import sys
18
19
 
20
+
19
21
  # Auto-initialize shutdown error suppression when logging package is imported
20
22
  def _initialize_shutdown_fixes():
21
23
  """Initialize shutdown error suppression when the package is imported."""
22
24
  try:
23
25
  from .context import _setup_shutdown_error_suppression
26
+
24
27
  _setup_shutdown_error_suppression()
25
28
  except ImportError:
26
29
  pass
27
30
 
31
+
28
32
  # Initialize when package is imported
29
33
  _initialize_shutdown_fixes()
30
34
 
31
35
  # Import internal modules in correct order to avoid circular imports
32
36
  # First, formatter has no internal dependencies
33
- from .formatter import StructuredFormatter
34
-
35
37
  # Second, context only depends on formatter
36
- from .context import LogContext, log_context, StructuredAdapter, get_logger
38
+ from .context import LogContext, StructuredAdapter, get_logger, log_context # noqa: E402
39
+ from .formatter import StructuredFormatter # noqa: E402
37
40
 
38
41
  # Third, helpers depend on context
39
- from .helpers import log_context_span, request_logging, log_tool_call
42
+ from .helpers import log_context_span, log_tool_call, request_logging # noqa: E402
40
43
 
41
44
  # Fourth, metrics depend on helpers and context
42
- from .metrics import metrics, MetricsLogger
45
+ from .metrics import MetricsLogger, metrics # noqa: E402
43
46
 
44
47
  __all__ = [
45
48
  "get_logger",
46
49
  "log_context",
47
50
  "LogContext",
51
+ "StructuredAdapter",
48
52
  "log_context_span",
49
53
  "request_logging",
50
54
  "log_tool_call",
@@ -53,6 +57,7 @@ __all__ = [
53
57
  "setup_logging",
54
58
  ]
55
59
 
60
+
56
61
  # --------------------------------------------------------------------------- #
57
62
  # Setup function for configuring logging
58
63
  # --------------------------------------------------------------------------- #
@@ -63,7 +68,7 @@ async def setup_logging(
63
68
  ) -> None:
64
69
  """
65
70
  Set up the logging system.
66
-
71
+
67
72
  Args:
68
73
  level: Logging level (default: INFO)
69
74
  structured: Whether to use structured JSON logging
@@ -72,39 +77,40 @@ async def setup_logging(
72
77
  # Get the root logger
73
78
  root_logger = logging.getLogger("chuk_tool_processor")
74
79
  root_logger.setLevel(level)
75
-
80
+
76
81
  # Create formatter
77
- formatter = StructuredFormatter() if structured else logging.Formatter(
78
- '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
82
+ formatter = (
83
+ StructuredFormatter()
84
+ if structured
85
+ else logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
79
86
  )
80
-
87
+
81
88
  # Always add a dummy handler and remove it to satisfy test expectations
82
89
  dummy_handler = logging.StreamHandler()
83
90
  root_logger.addHandler(dummy_handler)
84
91
  root_logger.removeHandler(dummy_handler)
85
-
92
+
86
93
  # Now clear any remaining handlers
87
94
  for handler in list(root_logger.handlers):
88
95
  root_logger.removeHandler(handler)
89
-
96
+
90
97
  # Add console handler
91
98
  console_handler = logging.StreamHandler(sys.stderr)
92
99
  console_handler.setLevel(level)
93
100
  console_handler.setFormatter(formatter)
94
101
  root_logger.addHandler(console_handler)
95
-
102
+
96
103
  # Add file handler if specified
97
104
  if log_file:
98
105
  file_handler = logging.FileHandler(log_file)
99
106
  file_handler.setLevel(level)
100
107
  file_handler.setFormatter(formatter)
101
108
  root_logger.addHandler(file_handler)
102
-
109
+
103
110
  # Log startup with internal logger
104
111
  internal_logger = logging.getLogger("chuk_tool_processor.logging")
105
112
  internal_logger.info(
106
- "Logging initialized",
107
- extra={"context": {"level": logging.getLevelName(level), "structured": structured}}
113
+ "Logging initialized", extra={"context": {"level": logging.getLevelName(level), "structured": structured}}
108
114
  )
109
115
 
110
116
 
@@ -115,4 +121,4 @@ root_logger.setLevel(logging.INFO)
115
121
  _handler = logging.StreamHandler(sys.stderr)
116
122
  _handler.setLevel(logging.INFO)
117
123
  _handler.setFormatter(StructuredFormatter())
118
- root_logger.addHandler(_handler)
124
+ root_logger.addHandler(_handler)