scitex 2.15.1__py3-none-any.whl → 2.15.2__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.
Files changed (107) hide show
  1. scitex/__init__.py +68 -61
  2. scitex/_mcp_tools/introspect.py +42 -23
  3. scitex/_mcp_tools/template.py +24 -0
  4. scitex/ai/classification/timeseries/_TimeSeriesSlidingWindowSplit.py +30 -1550
  5. scitex/ai/classification/timeseries/_sliding_window_core.py +467 -0
  6. scitex/ai/classification/timeseries/_sliding_window_plotting.py +369 -0
  7. scitex/audio/__init__.py +2 -2
  8. scitex/audio/_tts.py +18 -10
  9. scitex/audio/engines/base.py +17 -10
  10. scitex/audio/engines/elevenlabs_engine.py +1 -1
  11. scitex/canvas/editor/flask_editor/_core/__init__.py +27 -0
  12. scitex/canvas/editor/flask_editor/_core/_bbox_extraction.py +200 -0
  13. scitex/canvas/editor/flask_editor/_core/_editor.py +173 -0
  14. scitex/canvas/editor/flask_editor/_core/_export_helpers.py +353 -0
  15. scitex/canvas/editor/flask_editor/_core/_routes_basic.py +190 -0
  16. scitex/canvas/editor/flask_editor/_core/_routes_export.py +332 -0
  17. scitex/canvas/editor/flask_editor/_core/_routes_panels.py +252 -0
  18. scitex/canvas/editor/flask_editor/_core/_routes_save.py +218 -0
  19. scitex/canvas/editor/flask_editor/_core.py +25 -1684
  20. scitex/cli/introspect.py +112 -74
  21. scitex/cli/main.py +2 -0
  22. scitex/cli/plt.py +357 -0
  23. scitex/cli/repro.py +15 -8
  24. scitex/cli/resource.py +15 -8
  25. scitex/cli/scholar/__init__.py +15 -8
  26. scitex/cli/social.py +6 -6
  27. scitex/cli/stats.py +15 -8
  28. scitex/cli/template.py +129 -12
  29. scitex/cli/tex.py +15 -8
  30. scitex/cli/writer.py +15 -8
  31. scitex/cloud/__init__.py +41 -2
  32. scitex/config/_env_registry.py +84 -19
  33. scitex/context/__init__.py +22 -0
  34. scitex/dev/__init__.py +20 -1
  35. scitex/gen/__init__.py +50 -14
  36. scitex/gen/_list_packages.py +4 -4
  37. scitex/introspect/__init__.py +16 -9
  38. scitex/introspect/_core.py +7 -8
  39. scitex/{gen/_inspect_module.py → introspect/_list_api.py} +43 -54
  40. scitex/introspect/_mcp/__init__.py +10 -6
  41. scitex/introspect/_mcp/handlers.py +37 -12
  42. scitex/introspect/_members.py +7 -3
  43. scitex/introspect/_signature.py +3 -3
  44. scitex/introspect/_source.py +2 -2
  45. scitex/io/_save.py +1 -2
  46. scitex/logging/_formatters.py +19 -9
  47. scitex/mcp_server.py +1 -1
  48. scitex/os/__init__.py +4 -0
  49. scitex/{gen → os}/_check_host.py +4 -5
  50. scitex/plt/__init__.py +11 -14
  51. scitex/session/__init__.py +26 -7
  52. scitex/session/_decorator.py +1 -1
  53. scitex/sh/__init__.py +7 -4
  54. scitex/social/__init__.py +10 -8
  55. scitex/stats/_mcp/_handlers/__init__.py +31 -0
  56. scitex/stats/_mcp/_handlers/_corrections.py +113 -0
  57. scitex/stats/_mcp/_handlers/_descriptive.py +78 -0
  58. scitex/stats/_mcp/_handlers/_effect_size.py +106 -0
  59. scitex/stats/_mcp/_handlers/_format.py +94 -0
  60. scitex/stats/_mcp/_handlers/_normality.py +110 -0
  61. scitex/stats/_mcp/_handlers/_posthoc.py +224 -0
  62. scitex/stats/_mcp/_handlers/_power.py +247 -0
  63. scitex/stats/_mcp/_handlers/_recommend.py +102 -0
  64. scitex/stats/_mcp/_handlers/_run_test.py +279 -0
  65. scitex/stats/_mcp/_handlers/_stars.py +48 -0
  66. scitex/stats/_mcp/handlers.py +19 -1171
  67. scitex/stats/auto/_stat_style.py +175 -0
  68. scitex/stats/auto/_style_definitions.py +411 -0
  69. scitex/stats/auto/_styles.py +22 -620
  70. scitex/stats/descriptive/__init__.py +11 -8
  71. scitex/stats/descriptive/_ci.py +39 -0
  72. scitex/stats/power/_power.py +15 -4
  73. scitex/str/__init__.py +2 -1
  74. scitex/str/_title_case.py +63 -0
  75. scitex/template/__init__.py +25 -10
  76. scitex/template/_code_templates.py +147 -0
  77. scitex/template/_mcp/handlers.py +81 -0
  78. scitex/template/_mcp/tool_schemas.py +55 -0
  79. scitex/template/_templates/__init__.py +51 -0
  80. scitex/template/_templates/audio.py +233 -0
  81. scitex/template/_templates/canvas.py +312 -0
  82. scitex/template/_templates/capture.py +268 -0
  83. scitex/template/_templates/config.py +43 -0
  84. scitex/template/_templates/diagram.py +294 -0
  85. scitex/template/_templates/io.py +107 -0
  86. scitex/template/_templates/module.py +53 -0
  87. scitex/template/_templates/plt.py +202 -0
  88. scitex/template/_templates/scholar.py +267 -0
  89. scitex/template/_templates/session.py +130 -0
  90. scitex/template/_templates/session_minimal.py +43 -0
  91. scitex/template/_templates/session_plot.py +67 -0
  92. scitex/template/_templates/session_stats.py +77 -0
  93. scitex/template/_templates/stats.py +323 -0
  94. scitex/template/_templates/writer.py +296 -0
  95. scitex/ui/_backends/_email.py +10 -2
  96. scitex/ui/_backends/_webhook.py +5 -1
  97. scitex/web/_search_pubmed.py +10 -6
  98. {scitex-2.15.1.dist-info → scitex-2.15.2.dist-info}/METADATA +1 -1
  99. {scitex-2.15.1.dist-info → scitex-2.15.2.dist-info}/RECORD +105 -64
  100. scitex/gen/_ci.py +0 -12
  101. scitex/gen/_title_case.py +0 -89
  102. /scitex/{gen → context}/_detect_environment.py +0 -0
  103. /scitex/{gen → context}/_get_notebook_path.py +0 -0
  104. /scitex/{gen/_shell.py → sh/_shell_legacy.py} +0 -0
  105. {scitex-2.15.1.dist-info → scitex-2.15.2.dist-info}/WHEEL +0 -0
  106. {scitex-2.15.1.dist-info → scitex-2.15.2.dist-info}/entry_points.txt +0 -0
  107. {scitex-2.15.1.dist-info → scitex-2.15.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,353 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/canvas/editor/flask_editor/_core/_export_helpers.py
4
+
5
+ """Export and compose helpers for figure bundles."""
6
+
7
+ import io
8
+ import json as json_module
9
+ from pathlib import Path
10
+ from typing import TYPE_CHECKING, Any, Dict, List
11
+
12
+ if TYPE_CHECKING:
13
+ from .._core import WebEditor
14
+
15
+ __all__ = ["export_composed_figure", "compose_panels_to_figure"]
16
+
17
+
18
+ def export_composed_figure(
19
+ editor: "WebEditor",
20
+ formats: List[str] = None,
21
+ dpi: int = 150,
22
+ ) -> Dict[str, Any]:
23
+ """Compose and export figure to bundle.
24
+
25
+ Parameters
26
+ ----------
27
+ editor : WebEditor
28
+ The editor instance with panel_info.
29
+ formats : list of str
30
+ Output formats (default: ["png", "svg"]).
31
+ dpi : int
32
+ Resolution for raster output.
33
+
34
+ Returns
35
+ -------
36
+ dict
37
+ Result with 'success' and 'exported' keys.
38
+ """
39
+ if formats is None:
40
+ formats = ["png", "svg"]
41
+
42
+ import matplotlib
43
+
44
+ matplotlib.use("Agg")
45
+
46
+ import matplotlib.pyplot as plt
47
+
48
+ from scitex.io import ZipBundle
49
+
50
+ if not editor.panel_info:
51
+ return {"success": False, "error": "No panel info"}
52
+
53
+ bundle_path = editor.panel_info.get("bundle_path")
54
+ figure_dir = editor.panel_info.get("figure_dir")
55
+
56
+ if not bundle_path and not figure_dir:
57
+ return {"success": False, "error": "No bundle path"}
58
+
59
+ figure_name = (
60
+ Path(bundle_path).stem
61
+ if bundle_path
62
+ else (Path(figure_dir).stem.replace(".figure", "") if figure_dir else "figure")
63
+ )
64
+
65
+ # Read spec.json and layout.json
66
+ spec, layout_overrides = _read_spec_and_layout(
67
+ bundle_path, figure_dir, editor.panel_info
68
+ )
69
+
70
+ # Get figure dimensions
71
+ fig_width_mm, fig_height_mm = _get_figure_dimensions(spec)
72
+ fig_width_in = fig_width_mm / 25.4
73
+ fig_height_in = fig_height_mm / 25.4
74
+
75
+ fig = plt.figure(figsize=(fig_width_in, fig_height_in), dpi=dpi, facecolor="white")
76
+
77
+ # Compose panels
78
+ _compose_panels(
79
+ fig,
80
+ spec,
81
+ editor.panel_info,
82
+ layout_overrides,
83
+ fig_width_mm,
84
+ fig_height_mm,
85
+ )
86
+
87
+ exported = {}
88
+
89
+ # Save to bundle
90
+ if bundle_path:
91
+ with ZipBundle(bundle_path, mode="a") as bundle:
92
+ for fmt in formats:
93
+ buf = io.BytesIO()
94
+ fig.savefig(
95
+ buf,
96
+ format=fmt,
97
+ dpi=dpi,
98
+ bbox_inches="tight",
99
+ facecolor="white",
100
+ pad_inches=0.02,
101
+ )
102
+ buf.seek(0)
103
+ export_path = f"exports/{figure_name}.{fmt}"
104
+ bundle.write_bytes(export_path, buf.read())
105
+ exported[fmt] = export_path
106
+
107
+ plt.close(fig)
108
+ return {"success": True, "exported": exported}
109
+
110
+
111
+ def compose_panels_to_figure(
112
+ editor: "WebEditor",
113
+ fmt: str = "png",
114
+ dpi: int = 150,
115
+ ) -> io.BytesIO:
116
+ """Compose panels into a figure and return as BytesIO.
117
+
118
+ Parameters
119
+ ----------
120
+ editor : WebEditor
121
+ The editor instance.
122
+ fmt : str
123
+ Output format.
124
+ dpi : int
125
+ Resolution.
126
+
127
+ Returns
128
+ -------
129
+ io.BytesIO
130
+ The composed figure as bytes.
131
+ """
132
+ import matplotlib
133
+
134
+ matplotlib.use("Agg")
135
+ import matplotlib.pyplot as plt
136
+
137
+ bundle_path = editor.panel_info.get("bundle_path")
138
+ figure_dir = editor.panel_info.get("figure_dir")
139
+
140
+ spec, layout_overrides = _read_spec_and_layout(
141
+ bundle_path, figure_dir, editor.panel_info
142
+ )
143
+
144
+ fig_width_mm, fig_height_mm = _get_figure_dimensions(spec)
145
+ fig_width_in = fig_width_mm / 25.4
146
+ fig_height_in = fig_height_mm / 25.4
147
+
148
+ fig = plt.figure(
149
+ figsize=(fig_width_in, fig_height_in),
150
+ dpi=dpi,
151
+ facecolor="white",
152
+ )
153
+
154
+ _compose_panels(
155
+ fig,
156
+ spec,
157
+ editor.panel_info,
158
+ layout_overrides,
159
+ fig_width_mm,
160
+ fig_height_mm,
161
+ )
162
+
163
+ buf = io.BytesIO()
164
+ fig.savefig(
165
+ buf,
166
+ format=fmt if fmt != "jpg" else "jpeg",
167
+ dpi=dpi,
168
+ bbox_inches="tight",
169
+ facecolor="white",
170
+ pad_inches=0.02,
171
+ )
172
+ plt.close(fig)
173
+ buf.seek(0)
174
+ return buf
175
+
176
+
177
+ def _read_spec_and_layout(bundle_path, figure_dir, panel_info):
178
+ """Read spec.json and layout.json from bundle or directory."""
179
+ from scitex.io import ZipBundle
180
+
181
+ spec = {}
182
+ layout_overrides = {}
183
+
184
+ if bundle_path:
185
+ try:
186
+ with ZipBundle(bundle_path, mode="r") as bundle:
187
+ spec = bundle.read_json("spec.json")
188
+ try:
189
+ layout_overrides = bundle.read_json("layout.json")
190
+ except:
191
+ pass
192
+ except:
193
+ pass
194
+ elif figure_dir:
195
+ spec_path = Path(figure_dir) / "spec.json"
196
+ if spec_path.exists():
197
+ with open(spec_path) as f:
198
+ spec = json_module.load(f)
199
+ layout_path = Path(figure_dir) / "layout.json"
200
+ if layout_path.exists():
201
+ with open(layout_path) as f:
202
+ layout_overrides = json_module.load(f)
203
+
204
+ # In-memory layout overrides take precedence
205
+ if panel_info and panel_info.get("layout"):
206
+ layout_overrides = panel_info.get("layout", {})
207
+
208
+ return spec, layout_overrides
209
+
210
+
211
+ def _get_figure_dimensions(spec):
212
+ """Extract figure dimensions from spec."""
213
+ fig_width_mm = 180
214
+ fig_height_mm = 120
215
+
216
+ if "figure" in spec:
217
+ fig_info = spec.get("figure", {})
218
+ styles = fig_info.get("styles", {})
219
+ size = styles.get("size", {})
220
+ fig_width_mm = size.get("width_mm", 180)
221
+ fig_height_mm = size.get("height_mm", 120)
222
+
223
+ return fig_width_mm, fig_height_mm
224
+
225
+
226
+ def _compose_panels(
227
+ fig, spec, panel_info, layout_overrides, fig_width_mm, fig_height_mm
228
+ ):
229
+ """Compose panels onto the figure."""
230
+ import zipfile
231
+
232
+ import numpy as np
233
+ from PIL import Image
234
+
235
+ from scitex.io import ZipBundle
236
+
237
+ panels_spec = spec.get("panels", [])
238
+ panel_paths = panel_info.get("panel_paths", [])
239
+ panel_is_zip = panel_info.get("panel_is_zip", [])
240
+
241
+ exclude_patterns = ["hitmap", "overview", "thumb", "preview"]
242
+
243
+ for panel_spec in panels_spec:
244
+ panel_id = panel_spec.get("id", "")
245
+ pos = panel_spec.get("position", {})
246
+ size = panel_spec.get("size", {})
247
+
248
+ # Skip auxiliary panels
249
+ panel_id_lower = panel_id.lower()
250
+ if any(
251
+ skip in panel_id_lower for skip in ["overview", "thumb", "preview", "aux"]
252
+ ):
253
+ continue
254
+
255
+ # Find panel path
256
+ panel_path, panel_name, is_zip = _find_panel_path(
257
+ panel_id, panel_paths, panel_is_zip
258
+ )
259
+ if not panel_path:
260
+ continue
261
+
262
+ # Get layout override
263
+ override = layout_overrides.get(panel_name, {})
264
+ override_pos = override.get("position", {})
265
+ override_size = override.get("size", {})
266
+
267
+ x_mm = override_pos.get("x_mm", pos.get("x_mm", 0))
268
+ y_mm = override_pos.get("y_mm", pos.get("y_mm", 0))
269
+ w_mm = override_size.get("width_mm", size.get("width_mm", 60))
270
+ h_mm = override_size.get("height_mm", size.get("height_mm", 40))
271
+
272
+ x_frac = x_mm / fig_width_mm
273
+ y_frac = 1 - (y_mm + h_mm) / fig_height_mm
274
+ w_frac = w_mm / fig_width_mm
275
+ h_frac = h_mm / fig_height_mm
276
+
277
+ # Load and place panel image
278
+ try:
279
+ if is_zip:
280
+ with ZipBundle(panel_path, mode="r") as plot_bundle:
281
+ with zipfile.ZipFile(panel_path, "r") as zf:
282
+ png_files = [
283
+ n
284
+ for n in zf.namelist()
285
+ if n.endswith(".png")
286
+ and "exports/" in n
287
+ and not any(p in n.lower() for p in exclude_patterns)
288
+ ]
289
+ if png_files:
290
+ preview_path = png_files[0]
291
+ if ".plot/" in preview_path:
292
+ preview_path = preview_path.split(".plot/")[-1]
293
+ img_data = plot_bundle.read_bytes(preview_path)
294
+ img = Image.open(io.BytesIO(img_data))
295
+ ax = fig.add_axes([x_frac, y_frac, w_frac, h_frac])
296
+ ax.imshow(np.array(img))
297
+ ax.axis("off")
298
+ else:
299
+ plot_dir = Path(panel_path)
300
+ exports_dir = plot_dir / "exports"
301
+ if exports_dir.exists():
302
+ for png_file in exports_dir.glob("*.png"):
303
+ if not any(
304
+ p in png_file.name.lower() for p in exclude_patterns
305
+ ):
306
+ img = Image.open(png_file)
307
+ ax = fig.add_axes([x_frac, y_frac, w_frac, h_frac])
308
+ ax.imshow(np.array(img))
309
+ ax.axis("off")
310
+ break
311
+ except Exception as e:
312
+ print(f"Could not load panel {panel_id}: {e}")
313
+
314
+ # Draw panel letter
315
+ if panel_id and len(panel_id) <= 2:
316
+ letter_x = x_frac + 0.01
317
+ letter_y = y_frac + h_frac - 0.02
318
+ fig.text(
319
+ letter_x,
320
+ letter_y,
321
+ panel_id,
322
+ fontsize=14,
323
+ fontweight="bold",
324
+ color="black",
325
+ ha="left",
326
+ va="top",
327
+ transform=fig.transFigure,
328
+ bbox=dict(
329
+ boxstyle="square,pad=0.1",
330
+ facecolor="white",
331
+ edgecolor="none",
332
+ alpha=0.8,
333
+ ),
334
+ )
335
+
336
+
337
+ def _find_panel_path(panel_id, panel_paths, panel_is_zip):
338
+ """Find panel path matching the panel ID."""
339
+ for idx, pp in enumerate(panel_paths):
340
+ pp_name = Path(pp).stem.replace(".plot", "")
341
+ if (
342
+ pp_name == panel_id
343
+ or pp_name.startswith(f"panel_{panel_id}_")
344
+ or pp_name == f"panel_{panel_id}"
345
+ or f"_{panel_id}_" in pp_name
346
+ ):
347
+ panel_name = Path(pp).name
348
+ is_zip = panel_is_zip[idx] if idx < len(panel_is_zip) else False
349
+ return pp, panel_name, is_zip
350
+ return None, None, False
351
+
352
+
353
+ # EOF
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/canvas/editor/flask_editor/_core/_routes_basic.py
4
+
5
+ """Basic Flask routes for the editor."""
6
+
7
+ import base64
8
+ import json
9
+ from typing import TYPE_CHECKING
10
+
11
+ if TYPE_CHECKING:
12
+ from .._core import WebEditor
13
+
14
+ __all__ = [
15
+ "create_index_route",
16
+ "create_preview_route",
17
+ "create_hitmap_route",
18
+ "create_colormap_route",
19
+ "create_update_route",
20
+ "create_stats_route",
21
+ "create_shutdown_route",
22
+ ]
23
+
24
+
25
+ def create_index_route(app, editor: "WebEditor"):
26
+ """Create the index route."""
27
+ from flask import render_template_string
28
+
29
+ from ..templates import build_html_template
30
+
31
+ @app.route("/")
32
+ def index():
33
+ html_template = build_html_template()
34
+ json_path_str = str(editor.json_path.resolve())
35
+ figure_path = ""
36
+ panel_path = ""
37
+
38
+ if ".figure/" in json_path_str:
39
+ parts = json_path_str.split(".figure/")
40
+ figure_path = parts[0] + ".figure"
41
+ panel_path = parts[1] if len(parts) > 1 else ""
42
+ elif ".plot/" in json_path_str:
43
+ parts = json_path_str.split(".plot/")
44
+ figure_path = parts[0] + ".plot"
45
+ panel_path = parts[1] if len(parts) > 1 else ""
46
+ else:
47
+ figure_path = json_path_str
48
+
49
+ return render_template_string(
50
+ html_template,
51
+ filename=figure_path,
52
+ panel_path=panel_path,
53
+ overrides=json.dumps(editor.current_overrides),
54
+ )
55
+
56
+ return index
57
+
58
+
59
+ def create_preview_route(app, editor: "WebEditor"):
60
+ """Create the preview route."""
61
+ from flask import jsonify, request
62
+
63
+ from .._renderer import render_preview_with_bboxes
64
+
65
+ @app.route("/preview")
66
+ def preview():
67
+ dark_mode = request.args.get("dark_mode", "false").lower() == "true"
68
+ img_data, bboxes, img_size = render_preview_with_bboxes(
69
+ editor.csv_data,
70
+ editor.current_overrides,
71
+ metadata=editor.metadata,
72
+ dark_mode=dark_mode,
73
+ )
74
+ return jsonify(
75
+ {
76
+ "image": img_data,
77
+ "bboxes": bboxes,
78
+ "img_size": img_size,
79
+ "has_hitmap": editor.hitmap_path is not None
80
+ and editor.hitmap_path.exists(),
81
+ "format": "png",
82
+ "panel_info": editor.panel_info,
83
+ }
84
+ )
85
+
86
+ return preview
87
+
88
+
89
+ def create_hitmap_route(app, editor: "WebEditor"):
90
+ """Create the hitmap route."""
91
+ from flask import jsonify
92
+
93
+ @app.route("/hitmap")
94
+ def hitmap():
95
+ if editor.hitmap_path and editor.hitmap_path.exists():
96
+ with open(editor.hitmap_path, "rb") as f:
97
+ img_data = base64.b64encode(f.read()).decode("utf-8")
98
+ return jsonify(
99
+ {
100
+ "image": img_data,
101
+ "color_map": editor.color_map,
102
+ }
103
+ )
104
+ return jsonify({"error": "No hitmap available"}), 404
105
+
106
+ return hitmap
107
+
108
+
109
+ def create_colormap_route(app, editor: "WebEditor"):
110
+ """Create the color_map route."""
111
+ from flask import jsonify
112
+
113
+ @app.route("/color_map")
114
+ def color_map():
115
+ return jsonify(
116
+ {
117
+ "color_map": editor.color_map,
118
+ "hit_regions": editor.hit_regions,
119
+ }
120
+ )
121
+
122
+ return color_map
123
+
124
+
125
+ def create_update_route(app, editor: "WebEditor"):
126
+ """Create the update route."""
127
+ from flask import jsonify, request
128
+
129
+ from .._renderer import render_preview_with_bboxes
130
+
131
+ @app.route("/update", methods=["POST"])
132
+ def update():
133
+ data = request.json
134
+ editor.current_overrides.update(data.get("overrides", {}))
135
+ editor._user_modified = True
136
+ dark_mode = data.get("dark_mode", False)
137
+
138
+ img_data, bboxes, img_size = render_preview_with_bboxes(
139
+ editor.csv_data,
140
+ editor.current_overrides,
141
+ metadata=editor.metadata,
142
+ dark_mode=dark_mode,
143
+ )
144
+ return jsonify(
145
+ {
146
+ "image": img_data,
147
+ "bboxes": bboxes,
148
+ "img_size": img_size,
149
+ "status": "updated",
150
+ }
151
+ )
152
+
153
+ return update
154
+
155
+
156
+ def create_stats_route(app, editor: "WebEditor"):
157
+ """Create the stats route."""
158
+ from flask import jsonify
159
+
160
+ @app.route("/stats")
161
+ def stats():
162
+ stats_data = editor.metadata.get("stats", [])
163
+ stats_summary = editor.metadata.get("stats_summary", None)
164
+ return jsonify(
165
+ {
166
+ "stats": stats_data,
167
+ "stats_summary": stats_summary,
168
+ "has_stats": len(stats_data) > 0,
169
+ }
170
+ )
171
+
172
+ return stats
173
+
174
+
175
+ def create_shutdown_route(app, editor: "WebEditor"):
176
+ """Create the shutdown route."""
177
+ from flask import jsonify, request
178
+
179
+ @app.route("/shutdown", methods=["POST"])
180
+ def shutdown():
181
+ func = request.environ.get("werkzeug.server.shutdown")
182
+ if func is None:
183
+ raise RuntimeError("Not running with Werkzeug Server")
184
+ func()
185
+ return jsonify({"status": "shutdown"})
186
+
187
+ return shutdown
188
+
189
+
190
+ # EOF