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 +83 -20
- mermaid_trace/cli.py +144 -34
- mermaid_trace/core/context.py +102 -41
- mermaid_trace/core/decorators.py +322 -104
- mermaid_trace/core/events.py +160 -45
- mermaid_trace/core/formatter.py +107 -28
- mermaid_trace/handlers/async_handler.py +105 -0
- mermaid_trace/handlers/mermaid_handler.py +84 -51
- mermaid_trace/integrations/fastapi.py +94 -50
- {mermaid_trace-0.3.1.dist-info → mermaid_trace-0.4.1.dist-info}/METADATA +25 -8
- mermaid_trace-0.4.1.dist-info/RECORD +16 -0
- mermaid_trace-0.3.1.dist-info/RECORD +0 -15
- {mermaid_trace-0.3.1.dist-info → mermaid_trace-0.4.1.dist-info}/WHEEL +0 -0
- {mermaid_trace-0.3.1.dist-info → mermaid_trace-0.4.1.dist-info}/entry_points.txt +0 -0
- {mermaid_trace-0.3.1.dist-info → mermaid_trace-0.4.1.dist-info}/licenses/LICENSE +0 -0
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 .
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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__ = [
|
|
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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
#
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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()
|