mermaid-trace 0.3.1__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
@@ -7,57 +7,109 @@ developers understand the flow of their applications, debug complex interactions
7
7
  and document system behavior.
8
8
 
9
9
  Key Components:
10
- - `trace`: A decorator to instrument functions for tracing.
10
+ - `trace`: A decorator to instrument functions for tracing. It captures arguments,
11
+ return values, and errors, and logs them as interactions.
11
12
  - `LogContext`: Manages execution context (like thread-local storage) to track
12
13
  caller/callee relationships across async tasks and threads.
13
14
  - `configure_flow`: Sets up the logging handler to write diagrams to a file.
15
+ It handles handler configuration, file modes, and async logging setup.
16
+
17
+ Usage Example:
18
+ from mermaid_trace import trace, configure_flow
19
+
20
+ configure_flow("my_flow.mmd")
21
+
22
+ @trace
23
+ def hello():
24
+ print("Hello")
25
+
26
+ hello()
14
27
  """
15
28
 
16
29
  from .core.decorators import trace_interaction, trace
17
30
  from .handlers.mermaid_handler import MermaidFileHandler
18
- from .core.events import FlowEvent
31
+ from .handlers.async_handler import AsyncMermaidHandler
32
+ from .core.events import Event, FlowEvent
19
33
  from .core.context import LogContext
20
- from .core.formatter import MermaidFormatter
34
+ from .core.formatter import BaseFormatter, MermaidFormatter
21
35
  # We don't import integrations by default to avoid hard dependencies
22
36
  # Integrations (like FastAPI) must be imported explicitly by the user if needed.
23
37
 
24
38
  from importlib.metadata import PackageNotFoundError, version
39
+ from typing import List, Optional
25
40
 
26
41
  import logging
27
42
 
28
- def configure_flow(output_file: str = "flow.mmd") -> logging.Logger:
43
+
44
+ def configure_flow(
45
+ output_file: str = "flow.mmd",
46
+ handlers: Optional[List[logging.Handler]] = None,
47
+ append: bool = False,
48
+ async_mode: bool = False,
49
+ ) -> logging.Logger:
29
50
  """
30
51
  Configures the flow logger to output to a Mermaid file.
31
-
52
+
32
53
  This function sets up the logging infrastructure required to capture
33
54
  trace events and write them to the specified output file. It should
34
- be called once at the start of your application.
35
-
55
+ be called once at the start of your application to initialize the tracing system.
56
+
36
57
  Args:
37
58
  output_file (str): The absolute or relative path to the output .mmd file.
38
59
  Defaults to "flow.mmd" in the current directory.
39
-
60
+ If the file does not exist, it will be created with the correct header.
61
+ handlers (List[logging.Handler], optional): A list of custom logging handlers.
62
+ If provided, 'output_file' is ignored unless
63
+ you explicitly include a MermaidFileHandler.
64
+ Useful if you want to stream logs to other destinations.
65
+ append (bool): If True, adds the new handler(s) without removing existing ones.
66
+ Defaults to False (clears existing handlers to prevent duplicate logging).
67
+ async_mode (bool): If True, uses a non-blocking background thread for logging (QueueHandler).
68
+ Recommended for high-performance production environments to avoid
69
+ blocking the main execution thread during file I/O.
70
+ Defaults to False.
71
+
40
72
  Returns:
41
73
  logging.Logger: The configured logger instance used for flow tracing.
42
74
  """
43
75
  # Get the specific logger used by the tracing decorators
76
+ # This logger is isolated from the root logger to prevent pollution
44
77
  logger = logging.getLogger("mermaid_trace.flow")
45
78
  logger.setLevel(logging.INFO)
46
-
79
+
47
80
  # Remove existing handlers to avoid duplicate logs if configured multiple times
48
- if logger.hasHandlers():
81
+ # unless 'append' is requested. This ensures idempotency when calling configure_flow multiple times.
82
+ if not append and logger.hasHandlers():
49
83
  logger.handlers.clear()
50
-
51
- # Create and attach the custom handler that writes Mermaid syntax
52
- handler = MermaidFileHandler(output_file)
53
-
54
- # Use the custom formatter to convert FlowEvents to Mermaid strings
55
- handler.setFormatter(MermaidFormatter())
56
-
57
- logger.addHandler(handler)
58
-
84
+
85
+ # Determine the target handlers
86
+ target_handlers = []
87
+
88
+ if handlers:
89
+ # Use user-provided handlers if available
90
+ target_handlers = handlers
91
+ else:
92
+ # Create default Mermaid handler
93
+ # This handler knows how to write the Mermaid header and format events
94
+ handler = MermaidFileHandler(output_file)
95
+ handler.setFormatter(MermaidFormatter())
96
+ target_handlers = [handler]
97
+
98
+ if async_mode:
99
+ # Wrap the target handlers in an AsyncMermaidHandler (QueueHandler)
100
+ # The QueueListener will pick up logs from the queue and dispatch to target_handlers
101
+ # This decouples the application execution from the logging I/O
102
+ async_handler = AsyncMermaidHandler(target_handlers)
103
+ logger.addHandler(async_handler)
104
+ else:
105
+ # Attach handlers directly to the logger for synchronous logging
106
+ # Simple and reliable for debugging or low-throughput applications
107
+ for h in target_handlers:
108
+ logger.addHandler(h)
109
+
59
110
  return logger
60
111
 
112
+
61
113
  try:
62
114
  # Attempt to retrieve the installed package version
63
115
  __version__ = version("mermaid-trace")
@@ -67,4 +119,15 @@ except PackageNotFoundError:
67
119
 
68
120
 
69
121
  # Export public API for easy access
70
- __all__ = ["trace_interaction", "trace", "configure_flow", "MermaidFileHandler", "LogContext", "FlowEvent", "MermaidFormatter"]
122
+ __all__ = [
123
+ "trace_interaction",
124
+ "trace",
125
+ "configure_flow",
126
+ "MermaidFileHandler",
127
+ "AsyncMermaidHandler",
128
+ "LogContext",
129
+ "Event",
130
+ "FlowEvent",
131
+ "BaseFormatter",
132
+ "MermaidFormatter",
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,13 +15,21 @@ from pathlib import Path
8
15
  from typing import Type, Any
9
16
 
10
17
  try:
11
- from watchdog.observers import Observer
12
- from watchdog.events import FileSystemEventHandler
18
+ # Watchdog is an optional dependency for efficient file monitoring
19
+ # If installed, it enables instant file change detection
20
+ from watchdog.observers import Observer
21
+ from watchdog.events import FileSystemEventHandler
22
+
13
23
  HAS_WATCHDOG = True
14
24
  except ImportError:
25
+ # Fallback when watchdog is not installed (minimal install case)
15
26
  HAS_WATCHDOG = False
16
27
 
17
- # HTML Template for the preview page
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
18
33
  HTML_TEMPLATE = """
19
34
  <!DOCTYPE html>
20
35
  <html lang="en">
@@ -59,12 +74,15 @@ HTML_TEMPLATE = """
59
74
  mermaid.initialize({{ startOnLoad: true, theme: 'default' }});
60
75
 
61
76
  // Live Reload Logic
77
+ // We track the file's modification time (mtime) sent by the server.
62
78
  const currentMtime = "{mtime}";
63
79
 
64
80
  function checkUpdate() {{
81
+ // Poll the /_status endpoint to check if the file has changed on disk
65
82
  fetch('/_status')
66
83
  .then(response => response.text())
67
84
  .then(mtime => {{
85
+ // If the server reports a different mtime, reload the page
68
86
  if (mtime && mtime !== currentMtime) {{
69
87
  console.log("File changed, reloading...");
70
88
  location.reload();
@@ -74,76 +92,139 @@ HTML_TEMPLATE = """
74
92
  }}
75
93
 
76
94
  // Poll every 1 second
95
+ // This is a simple alternative to WebSockets for local dev tools
77
96
  setInterval(checkUpdate, 1000);
78
97
  </script>
79
98
  </body>
80
99
  </html>
81
100
  """
82
101
 
83
- def _create_handler(filename: str, path: Path) -> Type[http.server.SimpleHTTPRequestHandler]:
84
- """Helper to create the request handler class with closure over filename/path."""
102
+
103
+ def _create_handler(
104
+ filename: str, path: Path
105
+ ) -> Type[http.server.SimpleHTTPRequestHandler]:
106
+ """
107
+ Factory function to create a custom HTTP request handler class.
108
+
109
+ This uses a closure to inject `filename` and `path` into the handler's scope,
110
+ allowing the `do_GET` method to access them without global variables.
111
+
112
+ Args:
113
+ filename (str): Display name of the file being served
114
+ path (Path): Path object pointing to the file on disk
115
+
116
+ Returns:
117
+ Type[SimpleHTTPRequestHandler]: A custom request handler class
118
+ """
119
+
85
120
  class Handler(http.server.SimpleHTTPRequestHandler):
86
121
  """
87
- Custom Request Handler to serve the generated HTML dynamically.
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
88
128
  """
129
+
89
130
  def log_message(self, format: str, *args: Any) -> None:
90
- # Suppress default logging to keep console clean
131
+ """
132
+ Suppress default request logging to keep the console clean.
133
+
134
+ Only application logs are shown, not every HTTP request.
135
+ """
91
136
  pass
92
137
 
93
138
  def do_GET(self) -> None:
94
139
  """
95
- Handle GET requests.
96
- 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
97
146
  """
98
147
  if self.path == "/":
148
+ # Serve the main HTML page with embedded diagram
99
149
  self.send_response(200)
100
150
  self.send_header("Content-type", "text/html")
101
151
  self.end_headers()
102
-
152
+
103
153
  try:
104
- # Read the current content of the mermaid file
154
+ # Read current content of the Mermaid file
105
155
  content = path.read_text(encoding="utf-8")
106
156
  mtime = str(path.stat().st_mtime)
107
157
  except Exception as e:
108
- # Fallback if reading fails (e.g., file locked)
158
+ # Fallback if reading fails (e.g., file locked, permission error)
159
+ # Display error directly in the diagram area
109
160
  content = f"sequenceDiagram\nNote right of Error: Failed to read file: {e}"
110
161
  mtime = "0"
111
-
162
+
112
163
  # Inject content into the HTML template
113
- html = HTML_TEMPLATE.format(filename=filename, content=content, mtime=mtime)
164
+ html = HTML_TEMPLATE.format(
165
+ filename=filename, content=content, mtime=mtime
166
+ )
114
167
  self.wfile.write(html.encode("utf-8"))
115
-
168
+
116
169
  elif self.path == "/_status":
117
- # API endpoint for client to check file status
170
+ # API endpoint for client-side polling
171
+ # Returns current file modification time as plain text
118
172
  self.send_response(200)
119
173
  self.send_header("Content-type", "text/plain")
120
174
  self.end_headers()
121
175
  try:
122
176
  mtime = str(path.stat().st_mtime)
123
177
  except OSError:
178
+ # Fallback if file can't be accessed
124
179
  mtime = "0"
125
180
  self.wfile.write(mtime.encode("utf-8"))
126
-
181
+
127
182
  else:
128
- # Serve static files if needed, or return 404
183
+ # Serve other static files if needed, or return 404
129
184
  super().do_GET()
185
+
130
186
  return Handler
131
187
 
188
+
132
189
  def serve(filename: str, port: int = 8000) -> None:
133
190
  """
134
- Starts a local HTTP server to preview the Mermaid diagram.
191
+ Starts a local HTTP server to preview Mermaid diagrams in a web browser.
192
+
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.
196
+
197
+ Features:
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
202
+
203
+ Args:
204
+ filename (str): Path to the .mmd file to serve
205
+ port (int, optional): Port to bind the server to. Defaults to 8000.
135
206
  """
207
+ # Resolve the file path
136
208
  path = Path(filename)
137
209
  if not path.exists():
138
210
  print(f"Error: File '{filename}' not found.")
139
211
  sys.exit(1)
140
212
 
141
- # Setup Watchdog if available
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
142
216
  observer = None
143
217
  if HAS_WATCHDOG:
218
+
144
219
  class FileChangeHandler(FileSystemEventHandler):
220
+ """Watchdog event handler for detecting changes to the served file"""
221
+
145
222
  def on_modified(self, event: Any) -> None:
146
- if not event.is_directory and os.path.abspath(event.src_path) == str(path.resolve()):
223
+ """Called when a file is modified"""
224
+ # Filter only for modifications to our specific file
225
+ if not event.is_directory and os.path.abspath(event.src_path) == str(
226
+ path.resolve()
227
+ ):
147
228
  print(f"[Watchdog] File changed: {filename}")
148
229
 
149
230
  print("Initializing file watcher...")
@@ -151,43 +232,72 @@ def serve(filename: str, port: int = 8000) -> None:
151
232
  observer.schedule(FileChangeHandler(), path=str(path.parent), recursive=False)
152
233
  observer.start()
153
234
  else:
154
- print("Watchdog not installed. Falling back to polling mode (client-side only).")
235
+ print(
236
+ "Watchdog not installed. Falling back to polling mode (client-side only)."
237
+ )
155
238
 
239
+ # Create the custom HTTP handler
156
240
  HandlerClass = _create_handler(filename, path)
157
241
 
242
+ # Print server information
158
243
  print(f"Serving {filename} at http://localhost:{port}")
159
244
  print("Press Ctrl+C to stop.")
160
-
161
- # Open browser automatically to the server URL
245
+
246
+ # Automatically open the default web browser to the preview URL
162
247
  webbrowser.open(f"http://localhost:{port}")
163
-
248
+
164
249
  # Start the TCP server
250
+ # Using ThreadingTCPServer to handle multiple requests concurrently
251
+ # This ensures browser polling doesn't block the initial page load
165
252
  with socketserver.ThreadingTCPServer(("", port), HandlerClass) as httpd:
166
253
  try:
254
+ # Serve forever until interrupted
167
255
  httpd.serve_forever()
168
256
  except KeyboardInterrupt:
257
+ # Handle Ctrl+C gracefully
169
258
  print("\nStopping server...")
259
+ # Stop the watchdog observer if it was started
170
260
  if observer:
171
261
  observer.stop()
172
262
  observer.join()
263
+ # Close the server
173
264
  httpd.server_close()
174
265
 
266
+
175
267
  def main() -> None:
176
268
  """
177
269
  Entry point for the CLI application.
270
+
271
+ Parses command-line arguments and dispatches to the appropriate command handler.
272
+ Currently supports only the 'serve' command for previewing Mermaid diagrams.
178
273
  """
179
- parser = argparse.ArgumentParser(description="MermaidTrace CLI")
180
- subparsers = parser.add_subparsers(dest="command", required=True)
181
-
182
- # 'serve' command definition
183
- serve_parser = subparsers.add_parser("serve", help="Serve a Mermaid file in the browser")
184
- serve_parser.add_argument("file", help="Path to the .mmd file")
185
- serve_parser.add_argument("--port", type=int, default=8000, help="Port to bind to (default: 8000)")
186
-
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
+ )
283
+
284
+ # Define 'serve' command for previewing diagrams
285
+ serve_parser = subparsers.add_parser(
286
+ "serve", help="Serve a Mermaid file in the browser with live reload"
287
+ )
288
+ serve_parser.add_argument("file", help="Path to the .mmd file to serve")
289
+ serve_parser.add_argument(
290
+ "--port", type=int, default=8000, help="Port to bind to (default: 8000)"
291
+ )
292
+
293
+ # Parse arguments and execute command
187
294
  args = parser.parse_args()
188
-
295
+
189
296
  if args.command == "serve":
297
+ # Execute the serve command
190
298
  serve(args.file, args.port)
191
299
 
300
+
192
301
  if __name__ == "__main__":
302
+ # Run the main function when script is executed directly
193
303
  main()