mermaid-trace 0.4.0__py3-none-any.whl → 0.4.1__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.
mermaid_trace/__init__.py CHANGED
@@ -29,9 +29,9 @@ Usage Example:
29
29
  from .core.decorators import trace_interaction, trace
30
30
  from .handlers.mermaid_handler import MermaidFileHandler
31
31
  from .handlers.async_handler import AsyncMermaidHandler
32
- from .core.events import FlowEvent
32
+ from .core.events import Event, FlowEvent
33
33
  from .core.context import LogContext
34
- from .core.formatter import MermaidFormatter
34
+ from .core.formatter import BaseFormatter, MermaidFormatter
35
35
  # We don't import integrations by default to avoid hard dependencies
36
36
  # Integrations (like FastAPI) must be imported explicitly by the user if needed.
37
37
 
@@ -126,6 +126,8 @@ __all__ = [
126
126
  "MermaidFileHandler",
127
127
  "AsyncMermaidHandler",
128
128
  "LogContext",
129
+ "Event",
129
130
  "FlowEvent",
131
+ "BaseFormatter",
130
132
  "MermaidFormatter",
131
133
  ]
mermaid_trace/cli.py CHANGED
@@ -1,3 +1,10 @@
1
+ """
2
+ Command Line Interface Module
3
+
4
+ This module provides command-line functionality for MermaidTrace, primarily for
5
+ previewing generated Mermaid diagrams in a web browser with live reload capabilities.
6
+ """
7
+
1
8
  import argparse
2
9
  import http.server
3
10
  import socketserver
@@ -8,22 +15,21 @@ from pathlib import Path
8
15
  from typing import Type, Any
9
16
 
10
17
  try:
11
- # Watchdog is an optional dependency that allows efficient file monitoring.
12
- # If installed, we use it to detect file changes instantly.
18
+ # Watchdog is an optional dependency for efficient file monitoring
19
+ # If installed, it enables instant file change detection
13
20
  from watchdog.observers import Observer
14
21
  from watchdog.events import FileSystemEventHandler
15
22
 
16
23
  HAS_WATCHDOG = True
17
24
  except ImportError:
18
- # Fallback for when watchdog is not installed (e.g., minimal install).
25
+ # Fallback when watchdog is not installed (minimal install case)
19
26
  HAS_WATCHDOG = False
20
27
 
21
- # HTML Template for the preview page
22
- # This template provides a self-contained environment to render Mermaid diagrams.
23
- # It includes:
24
- # 1. Mermaid.js library from CDN.
25
- # 2. CSS for basic styling and layout.
26
- # 3. JavaScript logic for auto-refreshing when the source file changes.
28
+ # HTML Template for the diagram preview page
29
+ # Provides a self-contained environment to render Mermaid diagrams with:
30
+ # 1. Mermaid.js library from CDN for diagram rendering
31
+ # 2. Basic CSS styling for readability and layout
32
+ # 3. JavaScript for auto-refreshing when the source file changes
27
33
  HTML_TEMPLATE = """
28
34
  <!DOCTYPE html>
29
35
  <html lang="en">
@@ -98,47 +104,59 @@ def _create_handler(
98
104
  filename: str, path: Path
99
105
  ) -> Type[http.server.SimpleHTTPRequestHandler]:
100
106
  """
101
- Factory function to create a custom request handler class.
107
+ Factory function to create a custom HTTP request handler class.
102
108
 
103
109
  This uses a closure to inject `filename` and `path` into the handler's scope,
104
110
  allowing the `do_GET` method to access them without global variables.
105
111
 
106
112
  Args:
107
- filename (str): Display name of the file.
108
- path (Path): Path object to the file on disk.
113
+ filename (str): Display name of the file being served
114
+ path (Path): Path object pointing to the file on disk
109
115
 
110
116
  Returns:
111
- Type[SimpleHTTPRequestHandler]: A custom handler class.
117
+ Type[SimpleHTTPRequestHandler]: A custom request handler class
112
118
  """
113
119
 
114
120
  class Handler(http.server.SimpleHTTPRequestHandler):
115
121
  """
116
- Custom Request Handler to serve the generated HTML dynamically.
117
- It intercepts GET requests to serve the constructed HTML instead of static files.
122
+ Custom HTTP Request Handler for serving Mermaid diagram previews.
123
+
124
+ This handler intercepts GET requests to:
125
+ - Serve the HTML wrapper with embedded diagram content at the root path ('/')
126
+ - Provide file modification time for live reload at '/_status'
127
+ - Fall back to default behavior for other paths
118
128
  """
119
129
 
120
130
  def log_message(self, format: str, *args: Any) -> None:
121
- # Suppress default logging to keep console clean
122
- # We only want to see application logs, not every HTTP request
131
+ """
132
+ Suppress default request logging to keep the console clean.
133
+
134
+ Only application logs are shown, not every HTTP request.
135
+ """
123
136
  pass
124
137
 
125
138
  def do_GET(self) -> None:
126
139
  """
127
- Handle GET requests.
128
- Serves the HTML wrapper for the root path ('/').
140
+ Handle GET requests for different paths.
141
+
142
+ Routes:
143
+ - '/' : Serves HTML wrapper with embedded Mermaid content
144
+ - '/_status' : Returns current file modification time for live reload
145
+ - other paths : Falls back to default SimpleHTTPRequestHandler behavior
129
146
  """
130
147
  if self.path == "/":
148
+ # Serve the main HTML page with embedded diagram
131
149
  self.send_response(200)
132
150
  self.send_header("Content-type", "text/html")
133
151
  self.end_headers()
134
152
 
135
153
  try:
136
- # Read the current content of the mermaid file
154
+ # Read current content of the Mermaid file
137
155
  content = path.read_text(encoding="utf-8")
138
156
  mtime = str(path.stat().st_mtime)
139
157
  except Exception as e:
140
- # Fallback if reading fails (e.g., file locked)
141
- # Show the error directly in the diagram area
158
+ # Fallback if reading fails (e.g., file locked, permission error)
159
+ # Display error directly in the diagram area
142
160
  content = f"sequenceDiagram\nNote right of Error: Failed to read file: {e}"
143
161
  mtime = "0"
144
162
 
@@ -149,14 +167,15 @@ def _create_handler(
149
167
  self.wfile.write(html.encode("utf-8"))
150
168
 
151
169
  elif self.path == "/_status":
152
- # API endpoint for client-side polling.
153
- # Returns the current modification time of the file.
170
+ # API endpoint for client-side polling
171
+ # Returns current file modification time as plain text
154
172
  self.send_response(200)
155
173
  self.send_header("Content-type", "text/plain")
156
174
  self.end_headers()
157
175
  try:
158
176
  mtime = str(path.stat().st_mtime)
159
177
  except OSError:
178
+ # Fallback if file can't be accessed
160
179
  mtime = "0"
161
180
  self.wfile.write(mtime.encode("utf-8"))
162
181
 
@@ -169,35 +188,40 @@ def _create_handler(
169
188
 
170
189
  def serve(filename: str, port: int = 8000) -> None:
171
190
  """
172
- Starts a local HTTP server to preview the Mermaid diagram.
191
+ Starts a local HTTP server to preview Mermaid diagrams in a web browser.
173
192
 
174
- This function blocks the main thread and runs a TCP server.
175
- It automatically opens the default web browser to the preview URL.
193
+ This function blocks the main thread while running a TCP server. It automatically
194
+ opens the default web browser to the preview URL and supports live reload when
195
+ the source .mmd file changes.
176
196
 
177
197
  Features:
178
- - Serves the .mmd file wrapped in an HTML viewer.
179
- - Uses Watchdog (if available) or client-side polling for live reloads.
180
- - Gracefully handles shutdown on Ctrl+C.
198
+ - Serves .mmd files wrapped in an HTML viewer with Mermaid.js
199
+ - Live reload functionality using Watchdog (if available) or client-side polling
200
+ - Graceful shutdown handling on Ctrl+C
201
+ - Automatic browser opening
181
202
 
182
203
  Args:
183
- filename (str): Path to the .mmd file to serve.
184
- port (int): Port to bind the server to. Default is 8000.
204
+ filename (str): Path to the .mmd file to serve
205
+ port (int, optional): Port to bind the server to. Defaults to 8000.
185
206
  """
207
+ # Resolve the file path
186
208
  path = Path(filename)
187
209
  if not path.exists():
188
210
  print(f"Error: File '{filename}' not found.")
189
211
  sys.exit(1)
190
212
 
191
- # Setup Watchdog if available
192
- # Watchdog allows us to print console messages when the file changes.
193
- # The actual browser reload is triggered by the client polling the /_status endpoint,
194
- # but Watchdog gives immediate feedback in the terminal.
213
+ # Setup Watchdog file watcher if available
214
+ # Watchdog provides immediate file change notifications in the terminal
215
+ # The actual browser reload is handled by client-side polling
195
216
  observer = None
196
217
  if HAS_WATCHDOG:
197
218
 
198
219
  class FileChangeHandler(FileSystemEventHandler):
220
+ """Watchdog event handler for detecting changes to the served file"""
221
+
199
222
  def on_modified(self, event: Any) -> None:
200
- # Filter for the specific file we are watching
223
+ """Called when a file is modified"""
224
+ # Filter only for modifications to our specific file
201
225
  if not event.is_directory and os.path.abspath(event.src_path) == str(
202
226
  path.resolve()
203
227
  ):
@@ -212,50 +236,68 @@ def serve(filename: str, port: int = 8000) -> None:
212
236
  "Watchdog not installed. Falling back to polling mode (client-side only)."
213
237
  )
214
238
 
239
+ # Create the custom HTTP handler
215
240
  HandlerClass = _create_handler(filename, path)
216
241
 
242
+ # Print server information
217
243
  print(f"Serving {filename} at http://localhost:{port}")
218
244
  print("Press Ctrl+C to stop.")
219
245
 
220
- # Open browser automatically to the server URL
246
+ # Automatically open the default web browser to the preview URL
221
247
  webbrowser.open(f"http://localhost:{port}")
222
248
 
223
249
  # Start the TCP server
224
- # ThreadingTCPServer is used to handle multiple requests concurrently if needed,
225
- # ensuring the browser polling doesn't block the initial load.
250
+ # Using ThreadingTCPServer to handle multiple requests concurrently
251
+ # This ensures browser polling doesn't block the initial page load
226
252
  with socketserver.ThreadingTCPServer(("", port), HandlerClass) as httpd:
227
253
  try:
254
+ # Serve forever until interrupted
228
255
  httpd.serve_forever()
229
256
  except KeyboardInterrupt:
257
+ # Handle Ctrl+C gracefully
230
258
  print("\nStopping server...")
259
+ # Stop the watchdog observer if it was started
231
260
  if observer:
232
261
  observer.stop()
233
262
  observer.join()
263
+ # Close the server
234
264
  httpd.server_close()
235
265
 
236
266
 
237
267
  def main() -> None:
238
268
  """
239
269
  Entry point for the CLI application.
240
- Parses arguments and dispatches to the appropriate command handler.
270
+
271
+ Parses command-line arguments and dispatches to the appropriate command handler.
272
+ Currently supports only the 'serve' command for previewing Mermaid diagrams.
241
273
  """
242
- parser = argparse.ArgumentParser(description="MermaidTrace CLI")
243
- subparsers = parser.add_subparsers(dest="command", required=True)
274
+ # Create argument parser
275
+ parser = argparse.ArgumentParser(
276
+ description="MermaidTrace CLI - Preview Mermaid diagrams in browser"
277
+ )
278
+
279
+ # Add subparsers for different commands
280
+ subparsers = parser.add_subparsers(
281
+ dest="command", required=True, help="Available commands"
282
+ )
244
283
 
245
- # 'serve' command definition
284
+ # Define 'serve' command for previewing diagrams
246
285
  serve_parser = subparsers.add_parser(
247
- "serve", help="Serve a Mermaid file in the browser"
286
+ "serve", help="Serve a Mermaid file in the browser with live reload"
248
287
  )
249
- serve_parser.add_argument("file", help="Path to the .mmd file")
288
+ serve_parser.add_argument("file", help="Path to the .mmd file to serve")
250
289
  serve_parser.add_argument(
251
290
  "--port", type=int, default=8000, help="Port to bind to (default: 8000)"
252
291
  )
253
292
 
293
+ # Parse arguments and execute command
254
294
  args = parser.parse_args()
255
295
 
256
296
  if args.command == "serve":
297
+ # Execute the serve command
257
298
  serve(args.file, args.port)
258
299
 
259
300
 
260
301
  if __name__ == "__main__":
302
+ # Run the main function when script is executed directly
261
303
  main()
@@ -1,3 +1,12 @@
1
+ """
2
+ Log Context Management Module
3
+
4
+ This module provides a thread-safe, async-friendly context management system
5
+ for tracking execution context across the application. It uses Python's ContextVar
6
+ mechanism to ensure proper context propagation in both synchronous and asynchronous
7
+ environments.
8
+ """
9
+
1
10
  from contextvars import ContextVar, Token
2
11
  from contextlib import asynccontextmanager, contextmanager
3
12
  from typing import Any, AsyncIterator, Dict, Iterator
@@ -9,16 +18,14 @@ class LogContext:
9
18
  Manages global context information for logging (e.g., request_id, user_id, current_participant).
10
19
 
11
20
  This class utilizes `contextvars.ContextVar` to ensure thread-safety and
12
- correct context propagation in asynchronous (asyncio) environments.
13
- Unlike `threading.local()`, `ContextVar` works natively with Python's async/await
21
+ correct context propagation in asynchronous (asyncio) environments. Unlike
22
+ `threading.local()`, `ContextVar` works natively with Python's async/await
14
23
  event loop, ensuring that context is preserved across `await` points but isolated
15
24
  between different concurrent tasks.
16
25
  """
17
26
 
18
- # ContextVar is the key mechanism here.
19
- # It stores a dictionary unique to the current execution context (Task/Thread).
20
- # "log_context" is the name of the variable, useful for debugging.
21
- # The default value is implicitly an empty state if not set (handled in _get_store).
27
+ # ContextVar stores a dictionary unique to the current execution context (Task/Thread)
28
+ # The name "log_context" is used for debugging purposes
22
29
  _context_store: ContextVar[Dict[str, Any]] = ContextVar("log_context")
23
30
 
24
31
  @classmethod
@@ -27,13 +34,19 @@ class LogContext:
27
34
  Retrieves the current context dictionary.
28
35
 
29
36
  If the context variable has not been set in the current context,
30
- it returns a fresh empty dictionary. This prevents LookupError
31
- and ensures there's always a valid dictionary to work with.
37
+ it creates a fresh empty dictionary, sets it to the contextvar,
38
+ and returns it. This prevents LookupError and ensures there's
39
+ always a valid dictionary to work with.
40
+
41
+ Returns:
42
+ Dict[str, Any]: Current context dictionary for the execution flow
32
43
  """
33
44
  try:
34
45
  return cls._context_store.get()
35
46
  except LookupError:
36
- return {}
47
+ empty_dict: Dict[str, Any] = {}
48
+ cls._context_store.set(empty_dict)
49
+ return empty_dict
37
50
 
38
51
  @classmethod
39
52
  def set(cls, key: str, value: Any) -> None:
@@ -42,10 +55,14 @@ class LogContext:
42
55
 
43
56
  Important: ContextVars are immutable collections. To modify the context,
44
57
  we must:
45
- 1. Retrieve the current dictionary.
46
- 2. Create a shallow copy (to avoid affecting parent contexts if we were reusing the object).
47
- 3. Update the copy.
48
- 4. Re-set the ContextVar with the new dictionary.
58
+ 1. Retrieve the current dictionary using _get_store()
59
+ 2. Create a shallow copy to avoid affecting parent contexts
60
+ 3. Update the copy with the new key-value pair
61
+ 4. Re-set the ContextVar with the new dictionary
62
+
63
+ Args:
64
+ key (str): Name of the context variable to set
65
+ value (Any): Value to associate with the key
49
66
  """
50
67
  ctx = cls._get_store().copy()
51
68
  ctx[key] = value
@@ -57,7 +74,10 @@ class LogContext:
57
74
  Updates multiple keys in the current context at once.
58
75
 
59
76
  This follows the same Copy-Update-Set pattern as `set()` to maintain
60
- context isolation.
77
+ context isolation between different execution flows.
78
+
79
+ Args:
80
+ data (Dict[str, Any]): Dictionary of key-value pairs to update in context
61
81
  """
62
82
  if not data:
63
83
  return
@@ -69,6 +89,13 @@ class LogContext:
69
89
  def get(cls, key: str, default: Any = None) -> Any:
70
90
  """
71
91
  Retrieves a value from the current context safely.
92
+
93
+ Args:
94
+ key (str): Name of the context variable to retrieve
95
+ default (Any, optional): Default value if key doesn't exist. Defaults to None.
96
+
97
+ Returns:
98
+ Any: Value associated with the key, or default if key not found
72
99
  """
73
100
  return cls._get_store().get(key, default)
74
101
 
@@ -76,6 +103,9 @@ class LogContext:
76
103
  def get_all(cls) -> Dict[str, Any]:
77
104
  """
78
105
  Returns a copy of the entire context dictionary.
106
+
107
+ Returns:
108
+ Dict[str, Any]: Complete copy of the current context
79
109
  """
80
110
  return cls._get_store().copy()
81
111
 
@@ -92,11 +122,17 @@ class LogContext:
92
122
  # user_id reverts to previous value (or disappears) here
93
123
 
94
124
  Mechanism:
95
- 1. Copies current context and updates it with new data.
96
- 2. Sets the ContextVar to this new state, receiving a `Token`.
97
- 3. Yields control to the block.
125
+ 1. Copies current context and updates it with new data
126
+ 2. Sets the ContextVar to this new state, receiving a `Token`
127
+ 3. Yields control to the block
98
128
  4. Finally, uses the `Token` to reset the ContextVar to its exact state
99
- before the block entered.
129
+ before the block entered
130
+
131
+ Args:
132
+ data (Dict[str, Any]): Dictionary of context values to set within the scope
133
+
134
+ Yields:
135
+ None: Control to the block using this context manager
100
136
  """
101
137
  current_ctx = cls._get_store().copy()
102
138
  current_ctx.update(data)
@@ -104,7 +140,7 @@ class LogContext:
104
140
  try:
105
141
  yield
106
142
  finally:
107
- # Crucial: Reset restores the context to what it was before .set()
143
+ # Reset restores context to state before .set() was called
108
144
  cls._context_store.reset(token)
109
145
 
110
146
  @classmethod
@@ -120,6 +156,12 @@ class LogContext:
120
156
  This is functionally identical to `scope` but designed for `async with` blocks.
121
157
  It ensures that even if the code inside `yield` suspends execution (await),
122
158
  the context remains valid for that task.
159
+
160
+ Args:
161
+ data (Dict[str, Any]): Dictionary of context values to set within the scope
162
+
163
+ Yields:
164
+ None: Control to the async block using this context manager
123
165
  """
124
166
  current_ctx = cls._get_store().copy()
125
167
  current_ctx.update(data)
@@ -137,6 +179,12 @@ class LogContext:
137
179
  """
138
180
  Replaces the entire context with the provided data.
139
181
  Returns a Token that can be used to manually reset the context later.
182
+
183
+ Args:
184
+ data (Dict[str, Any]): New context dictionary to replace the current one
185
+
186
+ Returns:
187
+ Token[Dict[str, Any]]: Token for resetting context to previous state
140
188
  """
141
189
  return cls._context_store.set(data.copy())
142
190
 
@@ -144,21 +192,30 @@ class LogContext:
144
192
  def reset(cls, token: Token[Dict[str, Any]]) -> None:
145
193
  """
146
194
  Manually resets the context using a Token obtained from `set` or `set_all`.
195
+
196
+ Args:
197
+ token (Token[Dict[str, Any]]): Token returned by set_all() method
147
198
  """
148
199
  cls._context_store.reset(token)
149
200
 
150
201
  @classmethod
151
202
  def current_participant(cls) -> str:
152
203
  """
153
- Helper to get the 'participant' field, representing the current active object/module.
204
+ Helper method to get the 'participant' field, representing the current active object/module.
154
205
  Defaults to 'Unknown' if not set.
206
+
207
+ Returns:
208
+ str: Name of the current participant in the trace flow
155
209
  """
156
210
  return str(cls.get("participant", "Unknown"))
157
211
 
158
212
  @classmethod
159
213
  def set_participant(cls, name: str) -> None:
160
214
  """
161
- Helper to set the 'participant' field.
215
+ Helper method to set the 'participant' field.
216
+
217
+ Args:
218
+ name (str): Name of the participant to set
162
219
  """
163
220
  cls.set("participant", name)
164
221
 
@@ -170,9 +227,12 @@ class LogContext:
170
227
  Lazy Initialization Logic:
171
228
  If no trace_id exists in the current context, it generates a new UUIDv4
172
229
  and sets it immediately. This ensures that:
173
- 1. A trace ID is always available when asked for.
230
+ 1. A trace ID is always available when asked for
174
231
  2. Once generated, the same ID persists for the duration of the context
175
- (unless manually changed), linking all subsequent logs together.
232
+ (unless manually changed), linking all subsequent logs together
233
+
234
+ Returns:
235
+ str: Unique trace ID for the current execution flow
176
236
  """
177
237
  tid = cls.get("trace_id")
178
238
  if not tid: