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
scitex/cli/introspect.py CHANGED
@@ -23,20 +23,20 @@ def introspect(ctx, help_recursive):
23
23
 
24
24
  \b
25
25
  IPython-like introspection for any Python package:
26
- signature - Function/class signature (like func?)
27
- docstring - Extract docstrings
28
- source - Full source code (like func??)
29
- members - List module/class members (like dir())
30
- exports - Show __all__ exports
31
- examples - Find usage examples
26
+ q - Function/class signature (like func?)
27
+ qq - Full source code (like func??)
28
+ dir - List module/class members (like dir())
29
+ api - Full module API tree
30
+ docstring - Extract docstrings
31
+ exports - Show __all__ exports
32
+ examples - Find usage examples
32
33
 
33
34
  \b
34
35
  Examples:
35
- scitex introspect signature scitex.plt.plot
36
- scitex introspect docstring scitex.audio.speak --format parsed
37
- scitex introspect source scitex.stats.run_test --max-lines 50
38
- scitex introspect members scitex.plt --kind functions
39
- scitex introspect exports scitex.audio
36
+ scitex introspect q scitex.plt.plot
37
+ scitex introspect qq scitex.stats.run_test --max-lines 50
38
+ scitex introspect dir scitex.plt --kind functions
39
+ scitex introspect api scitex --max-depth 2
40
40
  """
41
41
  if help_recursive:
42
42
  from . import print_help_recursive
@@ -52,19 +52,19 @@ def introspect(ctx, help_recursive):
52
52
  @click.option("--no-defaults", is_flag=True, help="Exclude default values")
53
53
  @click.option("--no-annotations", is_flag=True, help="Exclude type annotations")
54
54
  @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
55
- def signature(dotted_path, no_defaults, no_annotations, as_json):
55
+ def q(dotted_path, no_defaults, no_annotations, as_json):
56
56
  """
57
57
  Get function/class signature (like IPython's func?)
58
58
 
59
59
  \b
60
60
  Examples:
61
- scitex introspect signature scitex.plt.plot
62
- scitex introspect signature scitex.audio.speak --json
63
- scitex introspect signature json.dumps
61
+ scitex introspect q scitex.plt.plot
62
+ scitex introspect q scitex.audio.speak --json
63
+ scitex introspect q json.dumps
64
64
  """
65
- from scitex.introspect import get_signature
65
+ from scitex.introspect import q as get_q
66
66
 
67
- result = get_signature(
67
+ result = get_q(
68
68
  dotted_path,
69
69
  include_defaults=not no_defaults,
70
70
  include_annotations=not no_annotations,
@@ -89,68 +89,23 @@ def signature(dotted_path, no_defaults, no_annotations, as_json):
89
89
  click.echo(line)
90
90
 
91
91
 
92
- @introspect.command()
93
- @click.argument("dotted_path")
94
- @click.option(
95
- "--format",
96
- "-f",
97
- type=click.Choice(["raw", "parsed", "summary"]),
98
- default="raw",
99
- help="Output format",
100
- )
101
- @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
102
- def docstring(dotted_path, format, as_json):
103
- """
104
- Get docstring of a Python object
105
-
106
- \b
107
- Formats:
108
- raw - Full docstring as-is
109
- parsed - Parse into sections (summary, parameters, returns, etc.)
110
- summary - First line/paragraph only
111
-
112
- \b
113
- Examples:
114
- scitex introspect docstring scitex.plt.plot
115
- scitex introspect docstring scitex.audio.speak --format parsed
116
- """
117
- from scitex.introspect import get_docstring
118
-
119
- result = get_docstring(dotted_path, format=format)
120
-
121
- if not result.get("success", False):
122
- click.secho(f"Error: {result.get('error', 'Unknown error')}", fg="red")
123
- sys.exit(1)
124
-
125
- if as_json:
126
- click.echo(json.dumps(result, indent=2))
127
- else:
128
- click.echo(result["docstring"])
129
- if format == "parsed" and result.get("sections"):
130
- click.echo("\n--- Parsed Sections ---")
131
- for key, value in result["sections"].items():
132
- if value:
133
- click.secho(f"\n[{key}]", fg="cyan", bold=True)
134
- click.echo(value)
135
-
136
-
137
92
  @introspect.command()
138
93
  @click.argument("dotted_path")
139
94
  @click.option("--max-lines", "-n", type=int, help="Limit output to N lines")
140
95
  @click.option("--no-decorators", is_flag=True, help="Exclude decorator lines")
141
96
  @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
142
- def source(dotted_path, max_lines, no_decorators, as_json):
97
+ def qq(dotted_path, max_lines, no_decorators, as_json):
143
98
  """
144
99
  Get source code of a Python object (like IPython's func??)
145
100
 
146
101
  \b
147
102
  Examples:
148
- scitex introspect source scitex.plt.plot
149
- scitex introspect source scitex.audio.speak --max-lines 50
103
+ scitex introspect qq scitex.plt.plot
104
+ scitex introspect qq scitex.audio.speak --max-lines 50
150
105
  """
151
- from scitex.introspect import get_source
106
+ from scitex.introspect import qq as get_qq
152
107
 
153
- result = get_source(
108
+ result = get_qq(
154
109
  dotted_path,
155
110
  max_lines=max_lines,
156
111
  include_decorators=not no_decorators,
@@ -169,7 +124,7 @@ def source(dotted_path, max_lines, no_decorators, as_json):
169
124
  click.echo(result["source"])
170
125
 
171
126
 
172
- @introspect.command()
127
+ @introspect.command("dir")
173
128
  @click.argument("dotted_path")
174
129
  @click.option(
175
130
  "--filter",
@@ -186,19 +141,19 @@ def source(dotted_path, max_lines, no_decorators, as_json):
186
141
  )
187
142
  @click.option("--inherited", is_flag=True, help="Include inherited members (classes)")
188
143
  @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
189
- def members(dotted_path, filter, kind, inherited, as_json):
144
+ def dir_cmd(dotted_path, filter, kind, inherited, as_json):
190
145
  """
191
146
  List members of a module or class (like dir())
192
147
 
193
148
  \b
194
149
  Examples:
195
- scitex introspect members scitex.plt
196
- scitex introspect members scitex.audio --kind functions
197
- scitex introspect members scitex.plt.AxisWrapper --filter all
150
+ scitex introspect dir scitex.plt
151
+ scitex introspect dir scitex.audio --kind functions
152
+ scitex introspect dir scitex.plt.AxisWrapper --filter all
198
153
  """
199
- from scitex.introspect import list_members
154
+ from scitex.introspect import dir as get_dir
200
155
 
201
- result = list_members(
156
+ result = get_dir(
202
157
  dotted_path,
203
158
  filter=filter,
204
159
  kind=kind,
@@ -220,6 +175,89 @@ def members(dotted_path, filter, kind, inherited, as_json):
220
175
  click.echo(f" {kind_str} {name_str}{summary}")
221
176
 
222
177
 
178
+ @introspect.command()
179
+ @click.argument("dotted_path")
180
+ @click.option("--max-depth", "-d", type=int, default=5, help="Max recursion depth")
181
+ @click.option("--docstring", is_flag=True, help="Include docstrings")
182
+ @click.option("--root-only", is_flag=True, help="Show only root-level items")
183
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
184
+ def api(dotted_path, max_depth, docstring, root_only, as_json):
185
+ """
186
+ List the full API tree of a module recursively
187
+
188
+ \b
189
+ Examples:
190
+ scitex introspect api scitex --max-depth 2
191
+ scitex introspect api scitex.plt --docstring
192
+ scitex introspect api scitex.audio --root-only
193
+ """
194
+ from scitex.introspect import list_api
195
+
196
+ df = list_api(
197
+ dotted_path,
198
+ max_depth=max_depth,
199
+ docstring=docstring,
200
+ root_only=root_only,
201
+ )
202
+
203
+ if as_json:
204
+ click.echo(json.dumps(df.to_dict(orient="records"), indent=2))
205
+ else:
206
+ click.secho(f"API tree of {dotted_path} ({len(df)} items):", fg="cyan")
207
+ for _, row in df.iterrows():
208
+ indent = " " * row["Depth"]
209
+ type_str = click.style(f"[{row['Type']}]", fg="yellow")
210
+ name = row["Name"].split(".")[-1]
211
+ name_str = click.style(name, fg="green", bold=True)
212
+ doc = f" - {row['Docstring'][:50]}..." if row.get("Docstring") else ""
213
+ click.echo(f"{indent}{type_str} {name_str}{doc}")
214
+
215
+
216
+ @introspect.command()
217
+ @click.argument("dotted_path")
218
+ @click.option(
219
+ "--format",
220
+ "-f",
221
+ type=click.Choice(["raw", "parsed", "summary"]),
222
+ default="raw",
223
+ help="Output format",
224
+ )
225
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
226
+ def docstring(dotted_path, format, as_json):
227
+ """
228
+ Get docstring of a Python object
229
+
230
+ \b
231
+ Formats:
232
+ raw - Full docstring as-is
233
+ parsed - Parse into sections (summary, parameters, returns, etc.)
234
+ summary - First line/paragraph only
235
+
236
+ \b
237
+ Examples:
238
+ scitex introspect docstring scitex.plt.plot
239
+ scitex introspect docstring scitex.audio.speak --format parsed
240
+ """
241
+ from scitex.introspect import get_docstring
242
+
243
+ result = get_docstring(dotted_path, format=format)
244
+
245
+ if not result.get("success", False):
246
+ click.secho(f"Error: {result.get('error', 'Unknown error')}", fg="red")
247
+ sys.exit(1)
248
+
249
+ if as_json:
250
+ click.echo(json.dumps(result, indent=2))
251
+ else:
252
+ click.echo(result["docstring"])
253
+ if format == "parsed" and result.get("sections"):
254
+ click.echo("\n--- Parsed Sections ---")
255
+ for key, value in result["sections"].items():
256
+ if value:
257
+ click.secho(f"\n[{key}]", fg="cyan", bold=True)
258
+ click.echo(value)
259
+
260
+
223
261
  @introspect.command()
224
262
  @click.argument("dotted_path")
225
263
  @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
scitex/cli/main.py CHANGED
@@ -17,6 +17,7 @@ from . import (
17
17
  convert,
18
18
  introspect,
19
19
  mcp,
20
+ plt,
20
21
  repro,
21
22
  resource,
22
23
  scholar,
@@ -71,6 +72,7 @@ cli.add_command(config.config)
71
72
  cli.add_command(convert.convert)
72
73
  cli.add_command(introspect.introspect)
73
74
  cli.add_command(mcp.mcp)
75
+ cli.add_command(plt.plt)
74
76
  cli.add_command(repro.repro)
75
77
  cli.add_command(resource.resource)
76
78
  cli.add_command(scholar.scholar)
scitex/cli/plt.py ADDED
@@ -0,0 +1,357 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: 2026-01-25
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/cli/plt.py
4
+
5
+ """
6
+ SciTeX CLI - Plot Commands
7
+
8
+ Thin wrapper around figrecipe CLI for reproducible matplotlib figures.
9
+ All commands delegate to figrecipe for reproducibility.
10
+ """
11
+
12
+ import subprocess
13
+ import sys
14
+
15
+ import click
16
+
17
+
18
+ def _run_figrecipe(*args) -> int:
19
+ """Run figrecipe CLI command."""
20
+ cmd = [sys.executable, "-m", "figrecipe"]
21
+ cmd.extend(args)
22
+
23
+ result = subprocess.run(cmd)
24
+ return result.returncode
25
+
26
+
27
+ def _check_figrecipe() -> bool:
28
+ """Check if figrecipe is available."""
29
+ try:
30
+ import figrecipe # noqa: F401
31
+
32
+ return True
33
+ except ImportError:
34
+ return False
35
+
36
+
37
+ @click.group(
38
+ context_settings={"help_option_names": ["-h", "--help"]},
39
+ invoke_without_command=True,
40
+ )
41
+ @click.option("--help-recursive", is_flag=True, help="Show help for all subcommands")
42
+ @click.pass_context
43
+ def plt(ctx, help_recursive):
44
+ """
45
+ Plot and figure management (powered by figrecipe)
46
+
47
+ \b
48
+ Commands:
49
+ plot - Create figure from YAML/JSON spec
50
+ edit - Launch interactive GUI editor
51
+ compose - Combine multiple figures
52
+ crop - Crop whitespace from images
53
+ reproduce - Reproduce figure from recipe
54
+ validate - Validate recipe reproducibility
55
+ diagram - Create diagrams (flowcharts, etc.)
56
+
57
+ \b
58
+ Examples:
59
+ scitex plt plot spec.yaml -o fig.png
60
+ scitex plt edit
61
+ scitex plt compose a.png b.png -o combined.png
62
+ scitex plt reproduce recipe.yaml
63
+
64
+ \b
65
+ Note: Wraps figrecipe CLI. Run 'figrecipe --help' for full options.
66
+ """
67
+ if not _check_figrecipe():
68
+ click.secho("Error: figrecipe not installed", fg="red", err=True)
69
+ click.echo("\nInstall with: pip install figrecipe")
70
+ ctx.exit(1)
71
+
72
+ if help_recursive:
73
+ from . import print_help_recursive
74
+
75
+ print_help_recursive(ctx, plt)
76
+ ctx.exit(0)
77
+ elif ctx.invoked_subcommand is None:
78
+ click.echo(ctx.get_help())
79
+
80
+
81
+ @plt.command()
82
+ @click.argument("spec_file", type=click.Path(exists=True))
83
+ @click.option("-o", "--output", required=True, help="Output file path")
84
+ @click.option("--dpi", type=int, default=300, help="DPI for raster output")
85
+ @click.option("--no-recipe", is_flag=True, help="Don't save YAML recipe")
86
+ def plot(spec_file, output, dpi, no_recipe):
87
+ """
88
+ Create a figure from a declarative YAML/JSON spec.
89
+
90
+ \b
91
+ Examples:
92
+ scitex plt plot spec.yaml -o fig.png
93
+ scitex plt plot spec.json -o fig.pdf --dpi 600
94
+ """
95
+ args = ["plot", spec_file, "-o", output, "--dpi", str(dpi)]
96
+ if no_recipe:
97
+ args.append("--no-recipe")
98
+ sys.exit(_run_figrecipe(*args))
99
+
100
+
101
+ @plt.command()
102
+ @click.argument("recipe_file", type=click.Path(exists=True), required=False)
103
+ def edit(recipe_file):
104
+ """
105
+ Launch interactive GUI editor.
106
+
107
+ \b
108
+ Examples:
109
+ scitex plt edit # Open blank editor
110
+ scitex plt edit recipe.yaml # Open existing recipe
111
+ """
112
+ args = ["edit"]
113
+ if recipe_file:
114
+ args.append(recipe_file)
115
+ sys.exit(_run_figrecipe(*args))
116
+
117
+
118
+ @plt.command()
119
+ @click.argument("sources", nargs=-1, required=True, type=click.Path(exists=True))
120
+ @click.option("-o", "--output", required=True, help="Output file path")
121
+ @click.option(
122
+ "-l",
123
+ "--layout",
124
+ type=click.Choice(["horizontal", "vertical", "grid"]),
125
+ default="horizontal",
126
+ help="Layout mode",
127
+ )
128
+ @click.option("--gap", type=float, default=5, help="Gap between panels (mm)")
129
+ @click.option("--dpi", type=int, default=300, help="DPI for output")
130
+ @click.option("--no-labels", is_flag=True, help="Don't add panel labels (A, B, C)")
131
+ @click.option("--caption", help="Figure caption")
132
+ def compose(sources, output, layout, gap, dpi, no_labels, caption):
133
+ """
134
+ Compose multiple figures into one.
135
+
136
+ \b
137
+ Examples:
138
+ scitex plt compose a.png b.png -o combined.png
139
+ scitex plt compose *.png -o fig.pdf --layout grid
140
+ scitex plt compose a.yaml b.yaml -o fig.png --gap 10
141
+ """
142
+ args = ["compose"]
143
+ args.extend(sources)
144
+ args.extend(
145
+ ["-o", output, "--layout", layout, "--gap", str(gap), "--dpi", str(dpi)]
146
+ )
147
+ if no_labels:
148
+ args.append("--no-labels")
149
+ if caption:
150
+ args.extend(["--caption", caption])
151
+ sys.exit(_run_figrecipe(*args))
152
+
153
+
154
+ @plt.command()
155
+ @click.argument("input_file", type=click.Path(exists=True))
156
+ @click.option("-o", "--output", help="Output file (default: overwrites input)")
157
+ @click.option("--margin", type=float, default=1, help="Margin to keep (mm)")
158
+ def crop(input_file, output, margin):
159
+ """
160
+ Crop whitespace from an image.
161
+
162
+ \b
163
+ Examples:
164
+ scitex plt crop figure.png
165
+ scitex plt crop figure.png -o cropped.png --margin 2
166
+ """
167
+ args = ["crop", input_file, "--margin", str(margin)]
168
+ if output:
169
+ args.extend(["-o", output])
170
+ sys.exit(_run_figrecipe(*args))
171
+
172
+
173
+ @plt.command()
174
+ @click.argument("recipe_file", type=click.Path(exists=True))
175
+ @click.option("-o", "--output", help="Output file path")
176
+ @click.option(
177
+ "--format", "fmt", type=click.Choice(["png", "pdf", "svg"]), help="Output format"
178
+ )
179
+ @click.option("--dpi", type=int, default=300, help="DPI for raster output")
180
+ def reproduce(recipe_file, output, fmt, dpi):
181
+ """
182
+ Reproduce a figure from a YAML recipe.
183
+
184
+ \b
185
+ Examples:
186
+ scitex plt reproduce recipe.yaml
187
+ scitex plt reproduce recipe.yaml -o new_fig.pdf
188
+ """
189
+ args = ["reproduce", recipe_file, "--dpi", str(dpi)]
190
+ if output:
191
+ args.extend(["-o", output])
192
+ if fmt:
193
+ args.extend(["--format", fmt])
194
+ sys.exit(_run_figrecipe(*args))
195
+
196
+
197
+ @plt.command()
198
+ @click.argument("recipe_file", type=click.Path(exists=True))
199
+ @click.option("--threshold", type=float, default=100, help="MSE threshold")
200
+ def validate(recipe_file, threshold):
201
+ """
202
+ Validate that a recipe reproduces its original figure.
203
+
204
+ \b
205
+ Examples:
206
+ scitex plt validate recipe.yaml
207
+ scitex plt validate recipe.yaml --threshold 50
208
+ """
209
+ args = ["validate", recipe_file, "--threshold", str(threshold)]
210
+ sys.exit(_run_figrecipe(*args))
211
+
212
+
213
+ @plt.command()
214
+ @click.argument("spec_file", type=click.Path(exists=True))
215
+ @click.option("-o", "--output", help="Output file path")
216
+ @click.option(
217
+ "--format", "fmt", type=click.Choice(["mermaid", "graphviz"]), default="mermaid"
218
+ )
219
+ def diagram(spec_file, output, fmt):
220
+ """
221
+ Create diagrams (flowcharts, pipelines, etc.)
222
+
223
+ \b
224
+ Examples:
225
+ scitex plt diagram workflow.yaml
226
+ scitex plt diagram pipeline.yaml -o diagram.png --format graphviz
227
+ """
228
+ args = ["diagram", spec_file, "--format", fmt]
229
+ if output:
230
+ args.extend(["-o", output])
231
+ sys.exit(_run_figrecipe(*args))
232
+
233
+
234
+ @plt.command()
235
+ @click.argument("recipe_file", type=click.Path(exists=True))
236
+ @click.option("-v", "--verbose", is_flag=True, help="Show detailed info")
237
+ def info(recipe_file, verbose):
238
+ """
239
+ Show information about a recipe.
240
+
241
+ \b
242
+ Examples:
243
+ scitex plt info recipe.yaml
244
+ scitex plt info recipe.yaml --verbose
245
+ """
246
+ args = ["info", recipe_file]
247
+ if verbose:
248
+ args.append("--verbose")
249
+ sys.exit(_run_figrecipe(*args))
250
+
251
+
252
+ @plt.command()
253
+ @click.argument("recipe_file", type=click.Path(exists=True))
254
+ @click.option("-o", "--output", help="Output file for extracted data")
255
+ def extract(recipe_file, output):
256
+ """
257
+ Extract plotted data arrays from a recipe.
258
+
259
+ \b
260
+ Examples:
261
+ scitex plt extract recipe.yaml
262
+ scitex plt extract recipe.yaml -o data.json
263
+ """
264
+ args = ["extract", recipe_file]
265
+ if output:
266
+ args.extend(["-o", output])
267
+ sys.exit(_run_figrecipe(*args))
268
+
269
+
270
+ @plt.command()
271
+ @click.argument("action", type=click.Choice(["list", "show", "set"]), default="list")
272
+ @click.argument("style_name", required=False)
273
+ def style(action, style_name):
274
+ """
275
+ Manage figure styles and presets.
276
+
277
+ \b
278
+ Examples:
279
+ scitex plt style list # List available styles
280
+ scitex plt style show nature # Show style details
281
+ scitex plt style set publication # Set default style
282
+ """
283
+ args = ["style", action]
284
+ if style_name:
285
+ args.append(style_name)
286
+ sys.exit(_run_figrecipe(*args))
287
+
288
+
289
+ @plt.command()
290
+ @click.option("--check", is_flag=True, help="Check font availability")
291
+ def fonts(check):
292
+ """
293
+ List or check available fonts.
294
+
295
+ \b
296
+ Examples:
297
+ scitex plt fonts
298
+ scitex plt fonts --check
299
+ """
300
+ args = ["fonts"]
301
+ if check:
302
+ args.append("--check")
303
+ sys.exit(_run_figrecipe(*args))
304
+
305
+
306
+ @plt.command()
307
+ @click.argument("input_file", type=click.Path(exists=True))
308
+ @click.option("-o", "--output", required=True, help="Output file path")
309
+ @click.option("--format", "fmt", help="Output format (inferred from extension)")
310
+ def convert(input_file, output, fmt):
311
+ """
312
+ Convert between figure formats.
313
+
314
+ \b
315
+ Examples:
316
+ scitex plt convert fig.png -o fig.pdf
317
+ scitex plt convert fig.svg -o fig.png
318
+ """
319
+ args = ["convert", input_file, "-o", output]
320
+ if fmt:
321
+ args.extend(["--format", fmt])
322
+ sys.exit(_run_figrecipe(*args))
323
+
324
+
325
+ @plt.command()
326
+ @click.option(
327
+ "-t",
328
+ "--transport",
329
+ type=click.Choice(["stdio", "sse", "http"]),
330
+ default="stdio",
331
+ help="Transport protocol",
332
+ )
333
+ @click.option("--host", default="0.0.0.0", help="Host for HTTP/SSE transport")
334
+ @click.option("--port", default=8087, type=int, help="Port for HTTP/SSE transport")
335
+ def serve(transport, host, port):
336
+ """
337
+ Run figrecipe MCP server
338
+
339
+ \b
340
+ Examples:
341
+ scitex plt serve # stdio for Claude Desktop
342
+ scitex plt serve -t http --port 8087
343
+ """
344
+ args = ["mcp", "run", "--transport", transport]
345
+ if transport != "stdio":
346
+ args.extend(["--host", host, "--port", str(port)])
347
+ click.secho(f"Starting figrecipe MCP server ({transport})", fg="cyan")
348
+ click.echo(f" Host: {host}")
349
+ click.echo(f" Port: {port}")
350
+
351
+ sys.exit(_run_figrecipe(*args))
352
+
353
+
354
+ if __name__ == "__main__":
355
+ plt()
356
+
357
+ # EOF
scitex/cli/repro.py CHANGED
@@ -10,8 +10,13 @@ import sys
10
10
  import click
11
11
 
12
12
 
13
- @click.group(context_settings={"help_option_names": ["-h", "--help"]})
14
- def repro():
13
+ @click.group(
14
+ context_settings={"help_option_names": ["-h", "--help"]},
15
+ invoke_without_command=True,
16
+ )
17
+ @click.option("--help-recursive", is_flag=True, help="Show help for all subcommands")
18
+ @click.pass_context
19
+ def repro(ctx, help_recursive):
15
20
  """
16
21
  Reproducibility utilities
17
22
 
@@ -29,20 +34,22 @@ def repro():
29
34
  scitex repro hash data.npy # Hash array file
30
35
  scitex repro seed 42 # Set random seed
31
36
  """
32
- pass
37
+ if help_recursive:
38
+ _print_help_recursive(ctx)
39
+ ctx.exit(0)
40
+ elif ctx.invoked_subcommand is None:
41
+ click.echo(ctx.get_help())
33
42
 
34
43
 
35
- @repro.command("help-recursive")
36
- @click.pass_context
37
- def help_recursive(ctx):
38
- """Show help for all commands recursively."""
44
+ def _print_help_recursive(ctx):
45
+ """Print help for all commands recursively."""
39
46
  fake_parent = click.Context(click.Group(), info_name="scitex")
40
47
  parent_ctx = click.Context(repro, info_name="repro", parent=fake_parent)
41
48
  click.secho("━━━ scitex repro ━━━", fg="cyan", bold=True)
42
49
  click.echo(repro.get_help(parent_ctx))
43
50
  for name in sorted(repro.list_commands(ctx) or []):
44
51
  cmd = repro.get_command(ctx, name)
45
- if cmd is None or name == "help-recursive":
52
+ if cmd is None:
46
53
  continue
47
54
  click.echo()
48
55
  click.secho(f"━━━ scitex repro {name} ━━━", fg="cyan", bold=True)