mermaid-trace 0.4.1__py3-none-any.whl → 0.5.3.post0__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 +53 -21
- mermaid_trace/cli.py +138 -84
- mermaid_trace/core/config.py +55 -0
- mermaid_trace/core/decorators.py +401 -196
- mermaid_trace/core/events.py +13 -143
- mermaid_trace/core/formatter.py +176 -18
- mermaid_trace/core/utils.py +96 -0
- mermaid_trace/handlers/async_handler.py +123 -47
- mermaid_trace/handlers/mermaid_handler.py +152 -86
- mermaid_trace/integrations/__init__.py +4 -0
- mermaid_trace/integrations/fastapi.py +101 -46
- {mermaid_trace-0.4.1.dist-info → mermaid_trace-0.5.3.post0.dist-info}/METADATA +57 -5
- mermaid_trace-0.5.3.post0.dist-info/RECORD +19 -0
- mermaid_trace-0.4.1.dist-info/RECORD +0 -16
- {mermaid_trace-0.4.1.dist-info → mermaid_trace-0.5.3.post0.dist-info}/WHEEL +0 -0
- {mermaid_trace-0.4.1.dist-info → mermaid_trace-0.5.3.post0.dist-info}/entry_points.txt +0 -0
- {mermaid_trace-0.4.1.dist-info → mermaid_trace-0.5.3.post0.dist-info}/licenses/LICENSE +0 -0
mermaid_trace/__init__.py
CHANGED
|
@@ -27,16 +27,41 @@ Usage Example:
|
|
|
27
27
|
"""
|
|
28
28
|
|
|
29
29
|
from .core.decorators import trace_interaction, trace
|
|
30
|
-
from .
|
|
30
|
+
from .core.utils import trace_class, patch_object
|
|
31
|
+
from .handlers.mermaid_handler import (
|
|
32
|
+
MermaidFileHandler,
|
|
33
|
+
RotatingMermaidFileHandler,
|
|
34
|
+
TimedRotatingMermaidFileHandler,
|
|
35
|
+
)
|
|
31
36
|
from .handlers.async_handler import AsyncMermaidHandler
|
|
32
37
|
from .core.events import Event, FlowEvent
|
|
33
38
|
from .core.context import LogContext
|
|
34
39
|
from .core.formatter import BaseFormatter, MermaidFormatter
|
|
40
|
+
from .core.config import config, MermaidConfig
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"trace_interaction",
|
|
44
|
+
"trace",
|
|
45
|
+
"trace_class",
|
|
46
|
+
"patch_object",
|
|
47
|
+
"MermaidFileHandler",
|
|
48
|
+
"RotatingMermaidFileHandler",
|
|
49
|
+
"TimedRotatingMermaidFileHandler",
|
|
50
|
+
"AsyncMermaidHandler",
|
|
51
|
+
"Event",
|
|
52
|
+
"FlowEvent",
|
|
53
|
+
"LogContext",
|
|
54
|
+
"BaseFormatter",
|
|
55
|
+
"MermaidFormatter",
|
|
56
|
+
"config",
|
|
57
|
+
"MermaidConfig",
|
|
58
|
+
"configure_flow",
|
|
59
|
+
]
|
|
35
60
|
# We don't import integrations by default to avoid hard dependencies
|
|
36
61
|
# Integrations (like FastAPI) must be imported explicitly by the user if needed.
|
|
37
62
|
|
|
38
63
|
from importlib.metadata import PackageNotFoundError, version
|
|
39
|
-
from typing import List, Optional
|
|
64
|
+
from typing import List, Optional, Dict, Any
|
|
40
65
|
|
|
41
66
|
import logging
|
|
42
67
|
|
|
@@ -45,8 +70,12 @@ def configure_flow(
|
|
|
45
70
|
output_file: str = "flow.mmd",
|
|
46
71
|
handlers: Optional[List[logging.Handler]] = None,
|
|
47
72
|
append: bool = False,
|
|
73
|
+
overwrite: bool = True,
|
|
48
74
|
async_mode: bool = False,
|
|
49
|
-
|
|
75
|
+
level: int = logging.INFO,
|
|
76
|
+
config_overrides: Optional[Dict[str, Any]] = None,
|
|
77
|
+
queue_size: Optional[int] = None,
|
|
78
|
+
) -> logging.Logger: # noqa: PLR0913
|
|
50
79
|
"""
|
|
51
80
|
Configures the flow logger to output to a Mermaid file.
|
|
52
81
|
|
|
@@ -64,18 +93,30 @@ def configure_flow(
|
|
|
64
93
|
Useful if you want to stream logs to other destinations.
|
|
65
94
|
append (bool): If True, adds the new handler(s) without removing existing ones.
|
|
66
95
|
Defaults to False (clears existing handlers to prevent duplicate logging).
|
|
96
|
+
overwrite (bool): If True, overwrites the output file if it already exists.
|
|
97
|
+
If False, appends to the existing file. Defaults to True.
|
|
67
98
|
async_mode (bool): If True, uses a non-blocking background thread for logging (QueueHandler).
|
|
68
99
|
Recommended for high-performance production environments to avoid
|
|
69
100
|
blocking the main execution thread during file I/O.
|
|
70
101
|
Defaults to False.
|
|
102
|
+
level (int): Logging level. Defaults to logging.INFO.
|
|
103
|
+
config_overrides (Dict[str, Any], optional): Dictionary to override default configuration settings.
|
|
104
|
+
Keys should match MermaidConfig attributes.
|
|
105
|
+
queue_size (int, optional): Size of the async queue. If provided, overrides config.queue_size.
|
|
71
106
|
|
|
72
107
|
Returns:
|
|
73
108
|
logging.Logger: The configured logger instance used for flow tracing.
|
|
74
109
|
"""
|
|
110
|
+
# Apply configuration overrides
|
|
111
|
+
if config_overrides:
|
|
112
|
+
for k, v in config_overrides.items():
|
|
113
|
+
if hasattr(config, k):
|
|
114
|
+
setattr(config, k, v)
|
|
115
|
+
|
|
75
116
|
# Get the specific logger used by the tracing decorators
|
|
76
117
|
# This logger is isolated from the root logger to prevent pollution
|
|
77
118
|
logger = logging.getLogger("mermaid_trace.flow")
|
|
78
|
-
logger.setLevel(
|
|
119
|
+
logger.setLevel(level)
|
|
79
120
|
|
|
80
121
|
# Remove existing handlers to avoid duplicate logs if configured multiple times
|
|
81
122
|
# unless 'append' is requested. This ensures idempotency when calling configure_flow multiple times.
|
|
@@ -91,15 +132,21 @@ def configure_flow(
|
|
|
91
132
|
else:
|
|
92
133
|
# Create default Mermaid handler
|
|
93
134
|
# This handler knows how to write the Mermaid header and format events
|
|
94
|
-
|
|
135
|
+
mode = "w" if overwrite else "a"
|
|
136
|
+
handler = MermaidFileHandler(output_file, mode=mode)
|
|
95
137
|
handler.setFormatter(MermaidFormatter())
|
|
96
138
|
target_handlers = [handler]
|
|
97
139
|
|
|
98
140
|
if async_mode:
|
|
141
|
+
# Determine queue size
|
|
142
|
+
final_queue_size = queue_size if queue_size is not None else config.queue_size
|
|
143
|
+
|
|
99
144
|
# Wrap the target handlers in an AsyncMermaidHandler (QueueHandler)
|
|
100
145
|
# The QueueListener will pick up logs from the queue and dispatch to target_handlers
|
|
101
146
|
# This decouples the application execution from the logging I/O
|
|
102
|
-
async_handler = AsyncMermaidHandler(
|
|
147
|
+
async_handler = AsyncMermaidHandler(
|
|
148
|
+
target_handlers, queue_size=final_queue_size
|
|
149
|
+
)
|
|
103
150
|
logger.addHandler(async_handler)
|
|
104
151
|
else:
|
|
105
152
|
# Attach handlers directly to the logger for synchronous logging
|
|
@@ -116,18 +163,3 @@ try:
|
|
|
116
163
|
except PackageNotFoundError:
|
|
117
164
|
# Fallback version if the package is not installed (e.g., local development)
|
|
118
165
|
__version__ = "0.0.0"
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
# Export public API for easy access
|
|
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,8 +1,20 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Command Line Interface Module
|
|
3
|
-
|
|
4
|
-
This module
|
|
5
|
-
|
|
2
|
+
Command Line Interface (CLI) Module for MermaidTrace.
|
|
3
|
+
|
|
4
|
+
This module serves as the entry point for the MermaidTrace command-line tools.
|
|
5
|
+
It provides functionality to:
|
|
6
|
+
1. **Serve** Mermaid diagram files (.mmd) via a local HTTP server.
|
|
7
|
+
2. **Preview** diagrams in a web browser with live-reload capabilities.
|
|
8
|
+
3. **Monitor** file changes using polling or filesystem events (via `watchdog`).
|
|
9
|
+
|
|
10
|
+
Key Components:
|
|
11
|
+
- `serve`: The primary command function that sets up the HTTP server and file watcher.
|
|
12
|
+
- `_create_handler`: A factory function that generates a custom request handler with access to the target file.
|
|
13
|
+
- `HTML_TEMPLATE`: A self-contained HTML page that renders Mermaid diagrams using the Mermaid.js CDN.
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
Run this module directly or via the `mermaid-trace` command (if installed).
|
|
17
|
+
Example: `python -m mermaid_trace.cli serve diagram.mmd --port 8080`
|
|
6
18
|
"""
|
|
7
19
|
|
|
8
20
|
import argparse
|
|
@@ -14,22 +26,28 @@ import os
|
|
|
14
26
|
from pathlib import Path
|
|
15
27
|
from typing import Type, Any
|
|
16
28
|
|
|
29
|
+
# Attempt to import `watchdog` for efficient file system monitoring.
|
|
30
|
+
# `watchdog` is an external library that allows the program to react to file events (like modifications) immediately.
|
|
31
|
+
# We handle the ImportError gracefully to allow the CLI to function (via polling) even if `watchdog` is not installed.
|
|
17
32
|
try:
|
|
18
|
-
# Watchdog is an optional dependency for efficient file monitoring
|
|
19
|
-
# If installed, it enables instant file change detection
|
|
20
33
|
from watchdog.observers import Observer
|
|
21
34
|
from watchdog.events import FileSystemEventHandler
|
|
22
35
|
|
|
23
36
|
HAS_WATCHDOG = True
|
|
24
37
|
except ImportError:
|
|
25
|
-
#
|
|
38
|
+
# If watchdog is missing, we fall back to a simpler polling mechanism in the browser
|
|
26
39
|
HAS_WATCHDOG = False
|
|
27
40
|
|
|
28
|
-
# HTML Template for the diagram preview page
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
41
|
+
# HTML Template for the diagram preview page.
|
|
42
|
+
# This string contains the full HTML structure served to the browser.
|
|
43
|
+
#
|
|
44
|
+
# It includes:
|
|
45
|
+
# 1. **Mermaid.js CDN**: Loads the library required to render the diagrams client-side.
|
|
46
|
+
# 2. **CSS Styling**: Basic styles for layout, readability, and the "Refresh" button.
|
|
47
|
+
# 3. **JavaScript Logic**:
|
|
48
|
+
# - Initializes Mermaid.js.
|
|
49
|
+
# - Implements a polling mechanism (`checkUpdate`) that hits the `/_status` endpoint.
|
|
50
|
+
# - Reloads the page automatically if the server reports a newer file modification time.
|
|
33
51
|
HTML_TEMPLATE = """
|
|
34
52
|
<!DOCTYPE html>
|
|
35
53
|
<html lang="en">
|
|
@@ -37,20 +55,30 @@ HTML_TEMPLATE = """
|
|
|
37
55
|
<meta charset="UTF-8">
|
|
38
56
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
39
57
|
<title>MermaidTrace Flow Preview</title>
|
|
40
|
-
<!-- Load Mermaid.js from CDN -->
|
|
58
|
+
<!-- Load Mermaid.js from CDN (Content Delivery Network) -->
|
|
59
|
+
<!-- This library parses the text-based diagram definition and renders it as an SVG -->
|
|
41
60
|
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
|
|
42
61
|
<style>
|
|
43
62
|
/* Basic styling for readability and layout */
|
|
44
63
|
body {{ font-family: sans-serif; padding: 20px; background: #f4f4f4; }}
|
|
64
|
+
|
|
65
|
+
/* Container for the diagram to give it a "card" look */
|
|
45
66
|
.container {{ background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }}
|
|
67
|
+
|
|
46
68
|
h1 {{ color: #333; }}
|
|
47
|
-
|
|
69
|
+
|
|
70
|
+
/* Allow horizontal scrolling for wide diagrams that might overflow the screen */
|
|
71
|
+
#diagram {{ overflow-x: auto; }}
|
|
72
|
+
|
|
73
|
+
/* Floating refresh button for manual reloads */
|
|
48
74
|
.refresh-btn {{
|
|
49
75
|
position: fixed; bottom: 20px; right: 20px;
|
|
50
76
|
padding: 10px 20px; background: #007bff; color: white;
|
|
51
77
|
border: none; border-radius: 5px; cursor: pointer; font-size: 16px;
|
|
52
78
|
}}
|
|
53
79
|
.refresh-btn:hover {{ background: #0056b3; }}
|
|
80
|
+
|
|
81
|
+
/* Status indicator to show the user that the live-reload is active */
|
|
54
82
|
.status {{
|
|
55
83
|
position: fixed; bottom: 20px; left: 20px;
|
|
56
84
|
font-size: 12px; color: #666;
|
|
@@ -60,29 +88,38 @@ HTML_TEMPLATE = """
|
|
|
60
88
|
<body>
|
|
61
89
|
<div class="container">
|
|
62
90
|
<h1>MermaidTrace Flow Preview: {filename}</h1>
|
|
63
|
-
<!-- The Mermaid diagram content will be injected here -->
|
|
91
|
+
<!-- The Mermaid diagram content will be injected here by the Python server -->
|
|
92
|
+
<!-- The 'mermaid' class triggers the Mermaid.js library to process this div -->
|
|
64
93
|
<div class="mermaid" id="diagram">
|
|
65
94
|
{content}
|
|
66
95
|
</div>
|
|
67
96
|
</div>
|
|
68
|
-
|
|
97
|
+
|
|
98
|
+
<!-- Button to manually reload the page/diagram if auto-reload fails or is slow -->
|
|
69
99
|
<button class="refresh-btn" onclick="location.reload()">Refresh Diagram</button>
|
|
70
100
|
<div class="status" id="status">Monitoring for changes...</div>
|
|
71
101
|
|
|
72
102
|
<script>
|
|
73
|
-
// Initialize Mermaid with default settings
|
|
103
|
+
// Initialize Mermaid with default settings.
|
|
104
|
+
// 'startOnLoad: true' tells Mermaid to find all .mermaid classes and render them immediately.
|
|
74
105
|
mermaid.initialize({{ startOnLoad: true, theme: 'default' }});
|
|
75
106
|
|
|
76
|
-
// Live Reload Logic
|
|
77
|
-
|
|
107
|
+
// --- Live Reload Logic ---
|
|
108
|
+
|
|
109
|
+
// We track the file's modification time (mtime) sent by the server during the initial page load.
|
|
110
|
+
// This value is injected into the template by Python.
|
|
78
111
|
const currentMtime = "{mtime}";
|
|
79
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Checks the server for updates to the source file.
|
|
115
|
+
* It fetches the '/_status' endpoint which returns the current file mtime.
|
|
116
|
+
*/
|
|
80
117
|
function checkUpdate() {{
|
|
81
|
-
// Poll the /_status endpoint to check if the file has changed on disk
|
|
82
118
|
fetch('/_status')
|
|
83
119
|
.then(response => response.text())
|
|
84
120
|
.then(mtime => {{
|
|
85
|
-
// If the server reports a different mtime
|
|
121
|
+
// If the server reports a different mtime than what we loaded with,
|
|
122
|
+
// it means the file has changed on disk. We reload the page to see the new diagram.
|
|
86
123
|
if (mtime && mtime !== currentMtime) {{
|
|
87
124
|
console.log("File changed, reloading...");
|
|
88
125
|
location.reload();
|
|
@@ -91,8 +128,9 @@ HTML_TEMPLATE = """
|
|
|
91
128
|
.catch(err => console.error("Error checking status:", err));
|
|
92
129
|
}}
|
|
93
130
|
|
|
94
|
-
// Poll every 1 second
|
|
95
|
-
// This is a simple alternative to WebSockets for local
|
|
131
|
+
// Poll every 1 second (1000 milliseconds).
|
|
132
|
+
// This is a simple, robust alternative to WebSockets for local development tools.
|
|
133
|
+
// It creates minimal load for a local server.
|
|
96
134
|
setInterval(checkUpdate, 1000);
|
|
97
135
|
</script>
|
|
98
136
|
</body>
|
|
@@ -106,81 +144,89 @@ def _create_handler(
|
|
|
106
144
|
"""
|
|
107
145
|
Factory function to create a custom HTTP request handler class.
|
|
108
146
|
|
|
109
|
-
|
|
110
|
-
|
|
147
|
+
We use a factory function (a function that returns a class) because `socketserver`
|
|
148
|
+
expects a class type, not an instance. This allows us to "close over" the `filename`
|
|
149
|
+
and `path` variables, making them available to the handler class without using globals.
|
|
111
150
|
|
|
112
151
|
Args:
|
|
113
|
-
filename (str):
|
|
114
|
-
path (Path): Path object pointing to the file on disk
|
|
152
|
+
filename (str): The display name of the file being served (used in the HTML title).
|
|
153
|
+
path (Path): The `pathlib.Path` object pointing to the actual file on disk.
|
|
115
154
|
|
|
116
155
|
Returns:
|
|
117
|
-
Type[SimpleHTTPRequestHandler]: A custom
|
|
156
|
+
Type[SimpleHTTPRequestHandler]: A custom class inheriting from `SimpleHTTPRequestHandler`.
|
|
118
157
|
"""
|
|
119
158
|
|
|
120
159
|
class Handler(http.server.SimpleHTTPRequestHandler):
|
|
121
160
|
"""
|
|
122
161
|
Custom HTTP Request Handler for serving Mermaid diagram previews.
|
|
123
162
|
|
|
124
|
-
This handler
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
- Fall back to default behavior for other paths
|
|
163
|
+
This handler overrides standard methods to provide two specific endpoints:
|
|
164
|
+
1. `/` (Root): Serves the HTML wrapper with the embedded diagram content.
|
|
165
|
+
2. `/_status`: Returns the file's current modification time (used for live reload).
|
|
128
166
|
"""
|
|
129
167
|
|
|
130
168
|
def log_message(self, format: str, *args: Any) -> None:
|
|
131
169
|
"""
|
|
132
|
-
|
|
170
|
+
Override `log_message` to suppress default HTTP request logging.
|
|
133
171
|
|
|
134
|
-
|
|
172
|
+
By default, `SimpleHTTPRequestHandler` logs every request to stderr.
|
|
173
|
+
We override this to keep the console output clean, showing only important application logs.
|
|
135
174
|
"""
|
|
136
175
|
pass
|
|
137
176
|
|
|
138
177
|
def do_GET(self) -> None:
|
|
139
178
|
"""
|
|
140
|
-
Handle GET requests
|
|
179
|
+
Handle HTTP GET requests.
|
|
141
180
|
|
|
142
181
|
Routes:
|
|
143
|
-
-
|
|
144
|
-
-
|
|
145
|
-
-
|
|
182
|
+
- **/**: Reads the target file, injects it into `HTML_TEMPLATE`, and serves the HTML.
|
|
183
|
+
- **/_status**: Checks the file's modification time and returns it as plain text.
|
|
184
|
+
- **Others**: Falls back to the default file serving behavior (though typically not used here).
|
|
146
185
|
"""
|
|
147
186
|
if self.path == "/":
|
|
148
|
-
# Serve the
|
|
187
|
+
# --- Root Endpoint: Serve the HTML Page ---
|
|
149
188
|
self.send_response(200)
|
|
150
189
|
self.send_header("Content-type", "text/html")
|
|
151
190
|
self.end_headers()
|
|
152
191
|
|
|
153
192
|
try:
|
|
154
|
-
# Read current content of the Mermaid file
|
|
193
|
+
# Read the current content of the Mermaid file from disk.
|
|
194
|
+
# We read it every time the page is requested to ensure we get the latest version.
|
|
155
195
|
content = path.read_text(encoding="utf-8")
|
|
196
|
+
# Get the modification time to embed in the page for the JS poller.
|
|
156
197
|
mtime = str(path.stat().st_mtime)
|
|
157
198
|
except Exception as e:
|
|
158
|
-
#
|
|
159
|
-
#
|
|
199
|
+
# Error Handling:
|
|
200
|
+
# If reading fails (e.g., file locked, permissions, deleted),
|
|
201
|
+
# we render a special Mermaid diagram showing the error message.
|
|
202
|
+
# This provides immediate visual feedback in the browser.
|
|
160
203
|
content = f"sequenceDiagram\nNote right of Error: Failed to read file: {e}"
|
|
161
204
|
mtime = "0"
|
|
162
205
|
|
|
163
|
-
# Inject
|
|
206
|
+
# Inject variables into the HTML template
|
|
164
207
|
html = HTML_TEMPLATE.format(
|
|
165
208
|
filename=filename, content=content, mtime=mtime
|
|
166
209
|
)
|
|
167
210
|
self.wfile.write(html.encode("utf-8"))
|
|
168
211
|
|
|
169
212
|
elif self.path == "/_status":
|
|
170
|
-
#
|
|
171
|
-
#
|
|
213
|
+
# --- Status Endpoint: Live Reload Polling ---
|
|
214
|
+
# The client-side JavaScript calls this endpoint periodically.
|
|
172
215
|
self.send_response(200)
|
|
173
216
|
self.send_header("Content-type", "text/plain")
|
|
174
217
|
self.end_headers()
|
|
175
218
|
try:
|
|
219
|
+
# Return the current modification time as the response body.
|
|
176
220
|
mtime = str(path.stat().st_mtime)
|
|
177
221
|
except OSError:
|
|
178
|
-
#
|
|
222
|
+
# If the file cannot be accessed (e.g., deleted), return "0".
|
|
179
223
|
mtime = "0"
|
|
180
224
|
self.wfile.write(mtime.encode("utf-8"))
|
|
181
225
|
|
|
182
226
|
else:
|
|
183
|
-
#
|
|
227
|
+
# --- Fallback: Default Behavior ---
|
|
228
|
+
# Useful if the HTML template referenced other static assets (images, css files),
|
|
229
|
+
# though currently everything is embedded.
|
|
184
230
|
super().do_GET()
|
|
185
231
|
|
|
186
232
|
return Handler
|
|
@@ -188,40 +234,43 @@ def _create_handler(
|
|
|
188
234
|
|
|
189
235
|
def serve(filename: str, port: int = 8000) -> None:
|
|
190
236
|
"""
|
|
191
|
-
Starts
|
|
237
|
+
Starts the local HTTP server and file watcher to preview a Mermaid diagram.
|
|
192
238
|
|
|
193
|
-
This
|
|
194
|
-
opens the
|
|
195
|
-
the source .mmd file changes.
|
|
239
|
+
This is the core logic for the `serve` command. It sets up the environment,
|
|
240
|
+
opens the browser, and enters a blocking loop to serve requests.
|
|
196
241
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
242
|
+
Workflow:
|
|
243
|
+
1. Validates the input file.
|
|
244
|
+
2. Sets up a `watchdog` observer (if installed) for console logging of changes.
|
|
245
|
+
3. Creates the custom HTTP handler using `_create_handler`.
|
|
246
|
+
4. Opens the user's default web browser to the server URL.
|
|
247
|
+
5. Starts a threaded TCP server to handle HTTP requests.
|
|
202
248
|
|
|
203
249
|
Args:
|
|
204
|
-
filename (str):
|
|
205
|
-
port (int
|
|
250
|
+
filename (str): The path to the .mmd file to serve.
|
|
251
|
+
port (int): The port number to bind the server to (default: 8000).
|
|
206
252
|
"""
|
|
207
|
-
#
|
|
253
|
+
# Create a Path object for robust file path handling
|
|
208
254
|
path = Path(filename)
|
|
255
|
+
|
|
256
|
+
# 1. Validation
|
|
209
257
|
if not path.exists():
|
|
210
258
|
print(f"Error: File '{filename}' not found.")
|
|
211
259
|
sys.exit(1)
|
|
212
260
|
|
|
213
|
-
#
|
|
214
|
-
#
|
|
215
|
-
# The
|
|
261
|
+
# 2. Watchdog Setup (Optional)
|
|
262
|
+
# If `watchdog` is installed, we use it to print immediate feedback to the console when the file changes.
|
|
263
|
+
# Note: The browser reload is driven by the JS polling the `/_status` endpoint, not by this observer.
|
|
264
|
+
# This observer is primarily for developer feedback in the terminal.
|
|
216
265
|
observer = None
|
|
217
266
|
if HAS_WATCHDOG:
|
|
218
267
|
|
|
219
268
|
class FileChangeHandler(FileSystemEventHandler):
|
|
220
|
-
"""
|
|
269
|
+
"""Internal handler class for Watchdog events."""
|
|
221
270
|
|
|
222
271
|
def on_modified(self, event: Any) -> None:
|
|
223
|
-
"""
|
|
224
|
-
#
|
|
272
|
+
"""Triggered when a file is modified in the watched directory."""
|
|
273
|
+
# We only care about modifications to the specific file we are serving.
|
|
225
274
|
if not event.is_directory and os.path.abspath(event.src_path) == str(
|
|
226
275
|
path.resolve()
|
|
227
276
|
):
|
|
@@ -229,6 +278,7 @@ def serve(filename: str, port: int = 8000) -> None:
|
|
|
229
278
|
|
|
230
279
|
print("Initializing file watcher...")
|
|
231
280
|
observer = Observer()
|
|
281
|
+
# Watch the directory containing the file, but filter events in the handler
|
|
232
282
|
observer.schedule(FileChangeHandler(), path=str(path.parent), recursive=False)
|
|
233
283
|
observer.start()
|
|
234
284
|
else:
|
|
@@ -236,52 +286,55 @@ def serve(filename: str, port: int = 8000) -> None:
|
|
|
236
286
|
"Watchdog not installed. Falling back to polling mode (client-side only)."
|
|
237
287
|
)
|
|
238
288
|
|
|
239
|
-
# Create
|
|
289
|
+
# 3. Create Server Handler
|
|
240
290
|
HandlerClass = _create_handler(filename, path)
|
|
241
291
|
|
|
242
|
-
#
|
|
292
|
+
# 4. User Feedback
|
|
243
293
|
print(f"Serving {filename} at http://localhost:{port}")
|
|
244
294
|
print("Press Ctrl+C to stop.")
|
|
245
295
|
|
|
246
|
-
#
|
|
296
|
+
# 5. Open Browser
|
|
297
|
+
# We open the browser *before* the server loop blocks, but the request might fail if the server
|
|
298
|
+
# isn't ready instantly. However, `socketserver` setup is usually fast enough.
|
|
247
299
|
webbrowser.open(f"http://localhost:{port}")
|
|
248
300
|
|
|
249
|
-
# Start
|
|
250
|
-
#
|
|
251
|
-
# This
|
|
301
|
+
# 6. Start Server
|
|
302
|
+
# We use `ThreadingTCPServer` so that multiple requests (e.g., polling + main page load)
|
|
303
|
+
# can be handled concurrently. This prevents the polling loop from blocking the page load.
|
|
252
304
|
with socketserver.ThreadingTCPServer(("", port), HandlerClass) as httpd:
|
|
253
305
|
try:
|
|
254
|
-
#
|
|
306
|
+
# Block and handle requests indefinitely
|
|
255
307
|
httpd.serve_forever()
|
|
256
308
|
except KeyboardInterrupt:
|
|
257
|
-
#
|
|
309
|
+
# 7. Graceful Shutdown
|
|
310
|
+
# Catch Ctrl+C to clean up resources properly
|
|
258
311
|
print("\nStopping server...")
|
|
259
|
-
# Stop the watchdog observer if it was started
|
|
260
312
|
if observer:
|
|
261
313
|
observer.stop()
|
|
262
314
|
observer.join()
|
|
263
|
-
# Close the server
|
|
264
315
|
httpd.server_close()
|
|
265
316
|
|
|
266
317
|
|
|
267
318
|
def main() -> None:
|
|
268
319
|
"""
|
|
269
|
-
|
|
320
|
+
Main entry point for the CLI application.
|
|
270
321
|
|
|
271
|
-
|
|
272
|
-
|
|
322
|
+
Responsibilities:
|
|
323
|
+
1. **Argument Parsing**: Uses `argparse` to define commands and options.
|
|
324
|
+
2. **Command Dispatch**: Calls the appropriate function based on the user's command.
|
|
273
325
|
"""
|
|
274
|
-
#
|
|
326
|
+
# Initialize the argument parser with a description of the tool
|
|
275
327
|
parser = argparse.ArgumentParser(
|
|
276
328
|
description="MermaidTrace CLI - Preview Mermaid diagrams in browser"
|
|
277
329
|
)
|
|
278
330
|
|
|
279
|
-
#
|
|
331
|
+
# Create sub-parsers to handle different commands (currently only 'serve')
|
|
280
332
|
subparsers = parser.add_subparsers(
|
|
281
333
|
dest="command", required=True, help="Available commands"
|
|
282
334
|
)
|
|
283
335
|
|
|
284
|
-
#
|
|
336
|
+
# --- 'serve' command ---
|
|
337
|
+
# Defines the 'serve' command which takes a file path and an optional port
|
|
285
338
|
serve_parser = subparsers.add_parser(
|
|
286
339
|
"serve", help="Serve a Mermaid file in the browser with live reload"
|
|
287
340
|
)
|
|
@@ -290,14 +343,15 @@ def main() -> None:
|
|
|
290
343
|
"--port", type=int, default=8000, help="Port to bind to (default: 8000)"
|
|
291
344
|
)
|
|
292
345
|
|
|
293
|
-
# Parse arguments
|
|
346
|
+
# Parse the arguments provided by the user
|
|
294
347
|
args = parser.parse_args()
|
|
295
348
|
|
|
349
|
+
# Dispatch logic
|
|
296
350
|
if args.command == "serve":
|
|
297
|
-
#
|
|
351
|
+
# Invoke the serve function with parsed arguments
|
|
298
352
|
serve(args.file, args.port)
|
|
299
353
|
|
|
300
354
|
|
|
301
355
|
if __name__ == "__main__":
|
|
302
|
-
#
|
|
356
|
+
# Standard boilerplate to run the main function when the script is executed directly
|
|
303
357
|
main()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration Module for Mermaid Trace
|
|
3
|
+
======================================
|
|
4
|
+
|
|
5
|
+
This module provides a centralized configuration system for the library.
|
|
6
|
+
It allows users to control behavior globally, such as argument capturing,
|
|
7
|
+
string truncation limits, and logging levels.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class MermaidConfig:
|
|
16
|
+
"""
|
|
17
|
+
Global configuration settings for Mermaid Trace.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
capture_args (bool): Whether to capture function arguments and return values.
|
|
21
|
+
Defaults to True. Set to False for performance or privacy.
|
|
22
|
+
max_string_length (int): Maximum length for string representations of objects.
|
|
23
|
+
Defaults to 50. Prevents huge log files.
|
|
24
|
+
max_arg_depth (int): Maximum recursion depth for nested objects (lists/dicts).
|
|
25
|
+
Defaults to 1.
|
|
26
|
+
queue_size (int): Size of the async queue. Defaults to 1000.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
capture_args: bool = True
|
|
30
|
+
max_string_length: int = 50
|
|
31
|
+
max_arg_depth: int = 1
|
|
32
|
+
queue_size: int = 1000
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def from_env(cls) -> "MermaidConfig":
|
|
36
|
+
"""
|
|
37
|
+
Loads configuration from environment variables.
|
|
38
|
+
|
|
39
|
+
Env Vars:
|
|
40
|
+
MERMAID_TRACE_CAPTURE_ARGS (bool): "true"/"false"
|
|
41
|
+
MERMAID_TRACE_MAX_STRING_LENGTH (int)
|
|
42
|
+
MERMAID_TRACE_MAX_ARG_DEPTH (int)
|
|
43
|
+
MERMAID_TRACE_QUEUE_SIZE (int)
|
|
44
|
+
"""
|
|
45
|
+
return cls(
|
|
46
|
+
capture_args=os.getenv("MERMAID_TRACE_CAPTURE_ARGS", "true").lower()
|
|
47
|
+
== "true",
|
|
48
|
+
max_string_length=int(os.getenv("MERMAID_TRACE_MAX_STRING_LENGTH", "50")),
|
|
49
|
+
max_arg_depth=int(os.getenv("MERMAID_TRACE_MAX_ARG_DEPTH", "1")),
|
|
50
|
+
queue_size=int(os.getenv("MERMAID_TRACE_QUEUE_SIZE", "1000")),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Global configuration instance
|
|
55
|
+
config = MermaidConfig()
|