mermaid-trace 0.4.0__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 +55 -21
- mermaid_trace/cli.py +159 -63
- mermaid_trace/core/config.py +55 -0
- mermaid_trace/core/context.py +83 -23
- mermaid_trace/core/decorators.py +440 -145
- mermaid_trace/core/events.py +46 -63
- mermaid_trace/core/formatter.py +257 -29
- mermaid_trace/core/utils.py +96 -0
- mermaid_trace/handlers/async_handler.py +156 -27
- mermaid_trace/handlers/mermaid_handler.py +162 -76
- mermaid_trace/integrations/__init__.py +4 -0
- mermaid_trace/integrations/fastapi.py +110 -34
- {mermaid_trace-0.4.0.dist-info → mermaid_trace-0.5.3.post0.dist-info}/METADATA +78 -11
- mermaid_trace-0.5.3.post0.dist-info/RECORD +19 -0
- mermaid_trace-0.4.0.dist-info/RECORD +0 -16
- {mermaid_trace-0.4.0.dist-info → mermaid_trace-0.5.3.post0.dist-info}/WHEEL +0 -0
- {mermaid_trace-0.4.0.dist-info → mermaid_trace-0.5.3.post0.dist-info}/entry_points.txt +0 -0
- {mermaid_trace-0.4.0.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
|
-
from .core.events import FlowEvent
|
|
37
|
+
from .core.events import Event, FlowEvent
|
|
33
38
|
from .core.context import LogContext
|
|
34
|
-
from .core.formatter import MermaidFormatter
|
|
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,16 +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
|
-
"FlowEvent",
|
|
130
|
-
"MermaidFormatter",
|
|
131
|
-
]
|
mermaid_trace/cli.py
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
"""
|
|
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`
|
|
18
|
+
"""
|
|
19
|
+
|
|
1
20
|
import argparse
|
|
2
21
|
import http.server
|
|
3
22
|
import socketserver
|
|
@@ -7,23 +26,28 @@ import os
|
|
|
7
26
|
from pathlib import Path
|
|
8
27
|
from typing import Type, Any
|
|
9
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.
|
|
10
32
|
try:
|
|
11
|
-
# Watchdog is an optional dependency that allows efficient file monitoring.
|
|
12
|
-
# If installed, we use it to detect file changes instantly.
|
|
13
33
|
from watchdog.observers import Observer
|
|
14
34
|
from watchdog.events import FileSystemEventHandler
|
|
15
35
|
|
|
16
36
|
HAS_WATCHDOG = True
|
|
17
37
|
except ImportError:
|
|
18
|
-
#
|
|
38
|
+
# If watchdog is missing, we fall back to a simpler polling mechanism in the browser
|
|
19
39
|
HAS_WATCHDOG = False
|
|
20
40
|
|
|
21
|
-
# HTML Template for the preview page
|
|
22
|
-
# This
|
|
41
|
+
# HTML Template for the diagram preview page.
|
|
42
|
+
# This string contains the full HTML structure served to the browser.
|
|
43
|
+
#
|
|
23
44
|
# It includes:
|
|
24
|
-
# 1.
|
|
25
|
-
# 2.
|
|
26
|
-
# 3.
|
|
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.
|
|
27
51
|
HTML_TEMPLATE = """
|
|
28
52
|
<!DOCTYPE html>
|
|
29
53
|
<html lang="en">
|
|
@@ -31,20 +55,30 @@ HTML_TEMPLATE = """
|
|
|
31
55
|
<meta charset="UTF-8">
|
|
32
56
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
33
57
|
<title>MermaidTrace Flow Preview</title>
|
|
34
|
-
<!-- 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 -->
|
|
35
60
|
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
|
|
36
61
|
<style>
|
|
37
62
|
/* Basic styling for readability and layout */
|
|
38
63
|
body {{ font-family: sans-serif; padding: 20px; background: #f4f4f4; }}
|
|
64
|
+
|
|
65
|
+
/* Container for the diagram to give it a "card" look */
|
|
39
66
|
.container {{ background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }}
|
|
67
|
+
|
|
40
68
|
h1 {{ color: #333; }}
|
|
41
|
-
|
|
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 */
|
|
42
74
|
.refresh-btn {{
|
|
43
75
|
position: fixed; bottom: 20px; right: 20px;
|
|
44
76
|
padding: 10px 20px; background: #007bff; color: white;
|
|
45
77
|
border: none; border-radius: 5px; cursor: pointer; font-size: 16px;
|
|
46
78
|
}}
|
|
47
79
|
.refresh-btn:hover {{ background: #0056b3; }}
|
|
80
|
+
|
|
81
|
+
/* Status indicator to show the user that the live-reload is active */
|
|
48
82
|
.status {{
|
|
49
83
|
position: fixed; bottom: 20px; left: 20px;
|
|
50
84
|
font-size: 12px; color: #666;
|
|
@@ -54,29 +88,38 @@ HTML_TEMPLATE = """
|
|
|
54
88
|
<body>
|
|
55
89
|
<div class="container">
|
|
56
90
|
<h1>MermaidTrace Flow Preview: {filename}</h1>
|
|
57
|
-
<!-- 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 -->
|
|
58
93
|
<div class="mermaid" id="diagram">
|
|
59
94
|
{content}
|
|
60
95
|
</div>
|
|
61
96
|
</div>
|
|
62
|
-
|
|
97
|
+
|
|
98
|
+
<!-- Button to manually reload the page/diagram if auto-reload fails or is slow -->
|
|
63
99
|
<button class="refresh-btn" onclick="location.reload()">Refresh Diagram</button>
|
|
64
100
|
<div class="status" id="status">Monitoring for changes...</div>
|
|
65
101
|
|
|
66
102
|
<script>
|
|
67
|
-
// 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.
|
|
68
105
|
mermaid.initialize({{ startOnLoad: true, theme: 'default' }});
|
|
69
106
|
|
|
70
|
-
// Live Reload Logic
|
|
71
|
-
|
|
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.
|
|
72
111
|
const currentMtime = "{mtime}";
|
|
73
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
|
+
*/
|
|
74
117
|
function checkUpdate() {{
|
|
75
|
-
// Poll the /_status endpoint to check if the file has changed on disk
|
|
76
118
|
fetch('/_status')
|
|
77
119
|
.then(response => response.text())
|
|
78
120
|
.then(mtime => {{
|
|
79
|
-
// 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.
|
|
80
123
|
if (mtime && mtime !== currentMtime) {{
|
|
81
124
|
console.log("File changed, reloading...");
|
|
82
125
|
location.reload();
|
|
@@ -85,8 +128,9 @@ HTML_TEMPLATE = """
|
|
|
85
128
|
.catch(err => console.error("Error checking status:", err));
|
|
86
129
|
}}
|
|
87
130
|
|
|
88
|
-
// Poll every 1 second
|
|
89
|
-
// 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.
|
|
90
134
|
setInterval(checkUpdate, 1000);
|
|
91
135
|
</script>
|
|
92
136
|
</body>
|
|
@@ -98,70 +142,91 @@ def _create_handler(
|
|
|
98
142
|
filename: str, path: Path
|
|
99
143
|
) -> Type[http.server.SimpleHTTPRequestHandler]:
|
|
100
144
|
"""
|
|
101
|
-
Factory function to create a custom request handler class.
|
|
145
|
+
Factory function to create a custom HTTP request handler class.
|
|
102
146
|
|
|
103
|
-
|
|
104
|
-
|
|
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.
|
|
105
150
|
|
|
106
151
|
Args:
|
|
107
|
-
filename (str):
|
|
108
|
-
path (Path): Path object 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.
|
|
109
154
|
|
|
110
155
|
Returns:
|
|
111
|
-
Type[SimpleHTTPRequestHandler]: A custom
|
|
156
|
+
Type[SimpleHTTPRequestHandler]: A custom class inheriting from `SimpleHTTPRequestHandler`.
|
|
112
157
|
"""
|
|
113
158
|
|
|
114
159
|
class Handler(http.server.SimpleHTTPRequestHandler):
|
|
115
160
|
"""
|
|
116
|
-
Custom Request Handler
|
|
117
|
-
|
|
161
|
+
Custom HTTP Request Handler for serving Mermaid diagram previews.
|
|
162
|
+
|
|
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).
|
|
118
166
|
"""
|
|
119
167
|
|
|
120
168
|
def log_message(self, format: str, *args: Any) -> None:
|
|
121
|
-
|
|
122
|
-
|
|
169
|
+
"""
|
|
170
|
+
Override `log_message` to suppress default HTTP request logging.
|
|
171
|
+
|
|
172
|
+
By default, `SimpleHTTPRequestHandler` logs every request to stderr.
|
|
173
|
+
We override this to keep the console output clean, showing only important application logs.
|
|
174
|
+
"""
|
|
123
175
|
pass
|
|
124
176
|
|
|
125
177
|
def do_GET(self) -> None:
|
|
126
178
|
"""
|
|
127
|
-
Handle GET requests.
|
|
128
|
-
|
|
179
|
+
Handle HTTP GET requests.
|
|
180
|
+
|
|
181
|
+
Routes:
|
|
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).
|
|
129
185
|
"""
|
|
130
186
|
if self.path == "/":
|
|
187
|
+
# --- Root Endpoint: Serve the HTML Page ---
|
|
131
188
|
self.send_response(200)
|
|
132
189
|
self.send_header("Content-type", "text/html")
|
|
133
190
|
self.end_headers()
|
|
134
191
|
|
|
135
192
|
try:
|
|
136
|
-
# Read the current content of the
|
|
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.
|
|
137
195
|
content = path.read_text(encoding="utf-8")
|
|
196
|
+
# Get the modification time to embed in the page for the JS poller.
|
|
138
197
|
mtime = str(path.stat().st_mtime)
|
|
139
198
|
except Exception as e:
|
|
140
|
-
#
|
|
141
|
-
#
|
|
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.
|
|
142
203
|
content = f"sequenceDiagram\nNote right of Error: Failed to read file: {e}"
|
|
143
204
|
mtime = "0"
|
|
144
205
|
|
|
145
|
-
# Inject
|
|
206
|
+
# Inject variables into the HTML template
|
|
146
207
|
html = HTML_TEMPLATE.format(
|
|
147
208
|
filename=filename, content=content, mtime=mtime
|
|
148
209
|
)
|
|
149
210
|
self.wfile.write(html.encode("utf-8"))
|
|
150
211
|
|
|
151
212
|
elif self.path == "/_status":
|
|
152
|
-
#
|
|
153
|
-
#
|
|
213
|
+
# --- Status Endpoint: Live Reload Polling ---
|
|
214
|
+
# The client-side JavaScript calls this endpoint periodically.
|
|
154
215
|
self.send_response(200)
|
|
155
216
|
self.send_header("Content-type", "text/plain")
|
|
156
217
|
self.end_headers()
|
|
157
218
|
try:
|
|
219
|
+
# Return the current modification time as the response body.
|
|
158
220
|
mtime = str(path.stat().st_mtime)
|
|
159
221
|
except OSError:
|
|
222
|
+
# If the file cannot be accessed (e.g., deleted), return "0".
|
|
160
223
|
mtime = "0"
|
|
161
224
|
self.wfile.write(mtime.encode("utf-8"))
|
|
162
225
|
|
|
163
226
|
else:
|
|
164
|
-
#
|
|
227
|
+
# --- Fallback: Default Behavior ---
|
|
228
|
+
# Useful if the HTML template referenced other static assets (images, css files),
|
|
229
|
+
# though currently everything is embedded.
|
|
165
230
|
super().do_GET()
|
|
166
231
|
|
|
167
232
|
return Handler
|
|
@@ -169,35 +234,43 @@ def _create_handler(
|
|
|
169
234
|
|
|
170
235
|
def serve(filename: str, port: int = 8000) -> None:
|
|
171
236
|
"""
|
|
172
|
-
Starts
|
|
237
|
+
Starts the local HTTP server and file watcher to preview a Mermaid diagram.
|
|
173
238
|
|
|
174
|
-
This
|
|
175
|
-
|
|
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.
|
|
176
241
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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.
|
|
181
248
|
|
|
182
249
|
Args:
|
|
183
|
-
filename (str):
|
|
184
|
-
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).
|
|
185
252
|
"""
|
|
253
|
+
# Create a Path object for robust file path handling
|
|
186
254
|
path = Path(filename)
|
|
255
|
+
|
|
256
|
+
# 1. Validation
|
|
187
257
|
if not path.exists():
|
|
188
258
|
print(f"Error: File '{filename}' not found.")
|
|
189
259
|
sys.exit(1)
|
|
190
260
|
|
|
191
|
-
#
|
|
192
|
-
#
|
|
193
|
-
# The
|
|
194
|
-
#
|
|
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.
|
|
195
265
|
observer = None
|
|
196
266
|
if HAS_WATCHDOG:
|
|
197
267
|
|
|
198
268
|
class FileChangeHandler(FileSystemEventHandler):
|
|
269
|
+
"""Internal handler class for Watchdog events."""
|
|
270
|
+
|
|
199
271
|
def on_modified(self, event: Any) -> None:
|
|
200
|
-
|
|
272
|
+
"""Triggered when a file is modified in the watched directory."""
|
|
273
|
+
# We only care about modifications to the specific file we are serving.
|
|
201
274
|
if not event.is_directory and os.path.abspath(event.src_path) == str(
|
|
202
275
|
path.resolve()
|
|
203
276
|
):
|
|
@@ -205,6 +278,7 @@ def serve(filename: str, port: int = 8000) -> None:
|
|
|
205
278
|
|
|
206
279
|
print("Initializing file watcher...")
|
|
207
280
|
observer = Observer()
|
|
281
|
+
# Watch the directory containing the file, but filter events in the handler
|
|
208
282
|
observer.schedule(FileChangeHandler(), path=str(path.parent), recursive=False)
|
|
209
283
|
observer.start()
|
|
210
284
|
else:
|
|
@@ -212,21 +286,28 @@ def serve(filename: str, port: int = 8000) -> None:
|
|
|
212
286
|
"Watchdog not installed. Falling back to polling mode (client-side only)."
|
|
213
287
|
)
|
|
214
288
|
|
|
289
|
+
# 3. Create Server Handler
|
|
215
290
|
HandlerClass = _create_handler(filename, path)
|
|
216
291
|
|
|
292
|
+
# 4. User Feedback
|
|
217
293
|
print(f"Serving {filename} at http://localhost:{port}")
|
|
218
294
|
print("Press Ctrl+C to stop.")
|
|
219
295
|
|
|
220
|
-
# Open
|
|
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.
|
|
221
299
|
webbrowser.open(f"http://localhost:{port}")
|
|
222
300
|
|
|
223
|
-
# Start
|
|
224
|
-
#
|
|
225
|
-
#
|
|
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.
|
|
226
304
|
with socketserver.ThreadingTCPServer(("", port), HandlerClass) as httpd:
|
|
227
305
|
try:
|
|
306
|
+
# Block and handle requests indefinitely
|
|
228
307
|
httpd.serve_forever()
|
|
229
308
|
except KeyboardInterrupt:
|
|
309
|
+
# 7. Graceful Shutdown
|
|
310
|
+
# Catch Ctrl+C to clean up resources properly
|
|
230
311
|
print("\nStopping server...")
|
|
231
312
|
if observer:
|
|
232
313
|
observer.stop()
|
|
@@ -236,26 +317,41 @@ def serve(filename: str, port: int = 8000) -> None:
|
|
|
236
317
|
|
|
237
318
|
def main() -> None:
|
|
238
319
|
"""
|
|
239
|
-
|
|
240
|
-
|
|
320
|
+
Main entry point for the CLI application.
|
|
321
|
+
|
|
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.
|
|
241
325
|
"""
|
|
242
|
-
parser
|
|
243
|
-
|
|
326
|
+
# Initialize the argument parser with a description of the tool
|
|
327
|
+
parser = argparse.ArgumentParser(
|
|
328
|
+
description="MermaidTrace CLI - Preview Mermaid diagrams in browser"
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
# Create sub-parsers to handle different commands (currently only 'serve')
|
|
332
|
+
subparsers = parser.add_subparsers(
|
|
333
|
+
dest="command", required=True, help="Available commands"
|
|
334
|
+
)
|
|
244
335
|
|
|
245
|
-
# 'serve' command
|
|
336
|
+
# --- 'serve' command ---
|
|
337
|
+
# Defines the 'serve' command which takes a file path and an optional port
|
|
246
338
|
serve_parser = subparsers.add_parser(
|
|
247
|
-
"serve", help="Serve a Mermaid file in the browser"
|
|
339
|
+
"serve", help="Serve a Mermaid file in the browser with live reload"
|
|
248
340
|
)
|
|
249
|
-
serve_parser.add_argument("file", help="Path to the .mmd file")
|
|
341
|
+
serve_parser.add_argument("file", help="Path to the .mmd file to serve")
|
|
250
342
|
serve_parser.add_argument(
|
|
251
343
|
"--port", type=int, default=8000, help="Port to bind to (default: 8000)"
|
|
252
344
|
)
|
|
253
345
|
|
|
346
|
+
# Parse the arguments provided by the user
|
|
254
347
|
args = parser.parse_args()
|
|
255
348
|
|
|
349
|
+
# Dispatch logic
|
|
256
350
|
if args.command == "serve":
|
|
351
|
+
# Invoke the serve function with parsed arguments
|
|
257
352
|
serve(args.file, args.port)
|
|
258
353
|
|
|
259
354
|
|
|
260
355
|
if __name__ == "__main__":
|
|
356
|
+
# Standard boilerplate to run the main function when the script is executed directly
|
|
261
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()
|