scitex 2.16.2__py3-none-any.whl → 2.17.3__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 (70) hide show
  1. scitex/_dev/__init__.py +122 -0
  2. scitex/_dev/_config.py +391 -0
  3. scitex/_dev/_dashboard/__init__.py +11 -0
  4. scitex/_dev/_dashboard/_app.py +89 -0
  5. scitex/_dev/_dashboard/_routes.py +169 -0
  6. scitex/_dev/_dashboard/_scripts.py +301 -0
  7. scitex/_dev/_dashboard/_styles.py +205 -0
  8. scitex/_dev/_dashboard/_templates.py +117 -0
  9. scitex/_dev/_dashboard/static/version-dashboard-favicon.svg +12 -0
  10. scitex/_dev/_ecosystem.py +109 -0
  11. scitex/_dev/_github.py +360 -0
  12. scitex/_dev/_mcp/__init__.py +11 -0
  13. scitex/_dev/_mcp/handlers.py +182 -0
  14. scitex/_dev/_ssh.py +332 -0
  15. scitex/_dev/_versions.py +272 -0
  16. scitex/_mcp_resources/_cheatsheet.py +1 -1
  17. scitex/_mcp_resources/_modules.py +1 -1
  18. scitex/_mcp_tools/__init__.py +4 -0
  19. scitex/_mcp_tools/dev.py +186 -0
  20. scitex/_mcp_tools/verify.py +256 -0
  21. scitex/audio/_audio_check.py +84 -41
  22. scitex/cli/capture.py +45 -22
  23. scitex/cli/dev.py +494 -0
  24. scitex/cli/main.py +4 -0
  25. scitex/cli/stats.py +48 -20
  26. scitex/cli/verify.py +473 -0
  27. scitex/dev/plt/__init__.py +1 -1
  28. scitex/dev/plt/mpl/get_dir_ax.py +1 -1
  29. scitex/dev/plt/mpl/get_signatures.py +1 -1
  30. scitex/dev/plt/mpl/get_signatures_details.py +1 -1
  31. scitex/io/_load.py +8 -1
  32. scitex/io/_save.py +12 -0
  33. scitex/plt/__init__.py +16 -6
  34. scitex/session/README.md +2 -2
  35. scitex/session/__init__.py +1 -0
  36. scitex/session/_decorator.py +57 -33
  37. scitex/session/_lifecycle/__init__.py +23 -0
  38. scitex/session/_lifecycle/_close.py +225 -0
  39. scitex/session/_lifecycle/_config.py +112 -0
  40. scitex/session/_lifecycle/_matplotlib.py +83 -0
  41. scitex/session/_lifecycle/_start.py +246 -0
  42. scitex/session/_lifecycle/_utils.py +186 -0
  43. scitex/session/_manager.py +40 -3
  44. scitex/session/template.py +1 -1
  45. scitex/template/__init__.py +18 -1
  46. scitex/template/_templates/plt.py +1 -1
  47. scitex/template/_templates/session.py +1 -1
  48. scitex/template/clone_research_minimal.py +111 -0
  49. scitex/verify/README.md +300 -0
  50. scitex/verify/__init__.py +208 -0
  51. scitex/verify/_chain.py +369 -0
  52. scitex/verify/_db.py +600 -0
  53. scitex/verify/_hash.py +187 -0
  54. scitex/verify/_integration.py +127 -0
  55. scitex/verify/_rerun.py +253 -0
  56. scitex/verify/_tracker.py +330 -0
  57. scitex/verify/_visualize.py +44 -0
  58. scitex/verify/_viz/__init__.py +38 -0
  59. scitex/verify/_viz/_colors.py +84 -0
  60. scitex/verify/_viz/_format.py +302 -0
  61. scitex/verify/_viz/_json.py +192 -0
  62. scitex/verify/_viz/_mermaid.py +440 -0
  63. scitex/verify/_viz/_templates.py +246 -0
  64. scitex/verify/_viz/_utils.py +56 -0
  65. {scitex-2.16.2.dist-info → scitex-2.17.3.dist-info}/METADATA +2 -1
  66. {scitex-2.16.2.dist-info → scitex-2.17.3.dist-info}/RECORD +69 -28
  67. scitex/session/_lifecycle.py +0 -827
  68. {scitex-2.16.2.dist-info → scitex-2.17.3.dist-info}/WHEEL +0 -0
  69. {scitex-2.16.2.dist-info → scitex-2.17.3.dist-info}/entry_points.txt +0 -0
  70. {scitex-2.16.2.dist-info → scitex-2.17.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-02-01 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/_mcp_tools/verify.py
4
+ """Verify module tools for FastMCP unified server."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ from typing import Optional
10
+
11
+
12
+ def _json(data: dict) -> str:
13
+ return json.dumps(data, indent=2, default=str)
14
+
15
+
16
+ def register_verify_tools(mcp) -> None:
17
+ """Register verify tools with FastMCP server."""
18
+
19
+ @mcp.tool()
20
+ async def verify_list(
21
+ limit: int = 50,
22
+ status_filter: Optional[str] = None,
23
+ ) -> str:
24
+ """[verify] List all tracked runs with verification status.
25
+
26
+ Parameters
27
+ ----------
28
+ limit : int, optional
29
+ Maximum number of runs to return (default: 50)
30
+ status_filter : str, optional
31
+ Filter by status: 'success', 'failed', 'running', or None for all
32
+
33
+ Returns
34
+ -------
35
+ str
36
+ JSON with list of runs and their verification status
37
+ """
38
+ from scitex.verify import get_db, verify_run
39
+
40
+ db = get_db()
41
+ runs = db.list_runs(status=status_filter, limit=limit)
42
+
43
+ results = []
44
+ for run in runs:
45
+ verification = verify_run(run["session_id"])
46
+ results.append(
47
+ {
48
+ "session_id": run["session_id"],
49
+ "script_path": run.get("script_path"),
50
+ "db_status": run.get("status"),
51
+ "verification_status": verification.status.value,
52
+ "is_verified": verification.is_verified,
53
+ "started_at": run.get("started_at"),
54
+ "finished_at": run.get("finished_at"),
55
+ }
56
+ )
57
+
58
+ return _json(
59
+ {
60
+ "count": len(results),
61
+ "runs": results,
62
+ }
63
+ )
64
+
65
+ @mcp.tool()
66
+ async def verify_run(
67
+ session_or_path: str,
68
+ ) -> str:
69
+ """[verify] Verify a specific session run by checking all file hashes.
70
+
71
+ Parameters
72
+ ----------
73
+ session_or_path : str
74
+ Session ID (e.g., '2025Y-11M-18D-09h12m03s_HmH5') or
75
+ path to a file to find its associated session
76
+
77
+ Returns
78
+ -------
79
+ str
80
+ JSON with verification results including file-level details
81
+ """
82
+ from pathlib import Path
83
+
84
+ from scitex.verify import get_db
85
+ from scitex.verify import verify_run as do_verify_run
86
+
87
+ db = get_db()
88
+
89
+ # Check if it's a file path
90
+ path = Path(session_or_path)
91
+ if path.exists():
92
+ sessions = db.find_session_by_file(str(path.resolve()), role="output")
93
+ if not sessions:
94
+ sessions = db.find_session_by_file(str(path.resolve()), role="input")
95
+
96
+ if not sessions:
97
+ return _json(
98
+ {
99
+ "error": f"No session found for file: {session_or_path}",
100
+ "session_id": None,
101
+ }
102
+ )
103
+ session_id = sessions[0]
104
+ else:
105
+ session_id = session_or_path
106
+
107
+ verification = do_verify_run(session_id)
108
+
109
+ return _json(
110
+ {
111
+ "session_id": verification.session_id,
112
+ "script_path": verification.script_path,
113
+ "status": verification.status.value,
114
+ "is_verified": verification.is_verified,
115
+ "combined_hash_expected": verification.combined_hash_expected,
116
+ "files": [
117
+ {
118
+ "path": f.path,
119
+ "role": f.role,
120
+ "status": f.status.value,
121
+ "expected_hash": f.expected_hash,
122
+ "current_hash": f.current_hash,
123
+ "is_verified": f.is_verified,
124
+ }
125
+ for f in verification.files
126
+ ],
127
+ "mismatched_count": len(verification.mismatched_files),
128
+ "missing_count": len(verification.missing_files),
129
+ }
130
+ )
131
+
132
+ @mcp.tool()
133
+ async def verify_chain(
134
+ target_file: str,
135
+ ) -> str:
136
+ """[verify] Verify the dependency chain for a target file.
137
+
138
+ Traces back through all sessions that contributed to producing
139
+ the target file and verifies each one.
140
+
141
+ Parameters
142
+ ----------
143
+ target_file : str
144
+ Path to the target file to trace
145
+
146
+ Returns
147
+ -------
148
+ str
149
+ JSON with chain verification results
150
+ """
151
+ from pathlib import Path
152
+
153
+ from scitex.verify import verify_chain as do_verify_chain
154
+
155
+ path = Path(target_file)
156
+ if not path.exists():
157
+ return _json(
158
+ {
159
+ "error": f"File not found: {target_file}",
160
+ "target_file": target_file,
161
+ }
162
+ )
163
+
164
+ chain = do_verify_chain(str(path.resolve()))
165
+
166
+ return _json(
167
+ {
168
+ "target_file": chain.target_file,
169
+ "status": chain.status.value,
170
+ "is_verified": chain.is_verified,
171
+ "chain_length": len(chain.runs),
172
+ "failed_runs_count": len(chain.failed_runs),
173
+ "runs": [
174
+ {
175
+ "session_id": r.session_id,
176
+ "script_path": r.script_path,
177
+ "status": r.status.value,
178
+ "is_verified": r.is_verified,
179
+ "mismatched_files": [f.path for f in r.mismatched_files],
180
+ "missing_files": [f.path for f in r.missing_files],
181
+ }
182
+ for r in chain.runs
183
+ ],
184
+ }
185
+ )
186
+
187
+ @mcp.tool()
188
+ async def verify_status() -> str:
189
+ """[verify] Show verification status summary (like git status).
190
+
191
+ Returns
192
+ -------
193
+ str
194
+ JSON with counts of verified, mismatched, and missing runs
195
+ """
196
+ from scitex.verify import get_status
197
+
198
+ status = get_status()
199
+ return _json(status)
200
+
201
+ @mcp.tool()
202
+ async def verify_stats() -> str:
203
+ """[verify] Show verification database statistics.
204
+
205
+ Returns
206
+ -------
207
+ str
208
+ JSON with database statistics
209
+ """
210
+ from scitex.verify import get_db
211
+
212
+ db = get_db()
213
+ stats = db.stats()
214
+ return _json(stats)
215
+
216
+ @mcp.tool()
217
+ async def verify_mermaid(
218
+ session_id: Optional[str] = None,
219
+ target_file: Optional[str] = None,
220
+ ) -> str:
221
+ """[verify] Generate Mermaid diagram for verification DAG.
222
+
223
+ Parameters
224
+ ----------
225
+ session_id : str, optional
226
+ Start from this session
227
+ target_file : str, optional
228
+ Start from session that produced this file
229
+
230
+ Returns
231
+ -------
232
+ str
233
+ Mermaid diagram code
234
+ """
235
+ from pathlib import Path
236
+
237
+ from scitex.verify import generate_mermaid_dag
238
+
239
+ if target_file:
240
+ target_file = str(Path(target_file).resolve())
241
+
242
+ mermaid_code = generate_mermaid_dag(
243
+ session_id=session_id,
244
+ target_file=target_file,
245
+ )
246
+
247
+ return _json(
248
+ {
249
+ "mermaid": mermaid_code,
250
+ "session_id": session_id,
251
+ "target_file": target_file,
252
+ }
253
+ )
254
+
255
+
256
+ # EOF
@@ -7,9 +7,75 @@ before attempting to play audio.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ import os
11
+ import shutil
10
12
  import subprocess
11
13
 
12
- __all__ = ["check_local_audio_available"]
14
+ __all__ = ["check_local_audio_available", "check_wsl_windows_audio_available"]
15
+
16
+
17
+ def check_wsl_windows_audio_available() -> dict:
18
+ """Check if WSL Windows audio playback is available.
19
+
20
+ In WSL, audio can be played via PowerShell's System.Media.SoundPlayer
21
+ even when PulseAudio is unavailable or SUSPENDED.
22
+
23
+ Returns
24
+ -------
25
+ dict with keys:
26
+ - available: bool - True if Windows playback via PowerShell is possible
27
+ - reason: str - Human-readable explanation
28
+ """
29
+ if not os.path.exists("/mnt/c/Windows"):
30
+ return {"available": False, "reason": "Not running in WSL"}
31
+
32
+ if not shutil.which("powershell.exe"):
33
+ return {"available": False, "reason": "powershell.exe not found in PATH"}
34
+
35
+ return {
36
+ "available": True,
37
+ "reason": "WSL Windows playback available via PowerShell",
38
+ }
39
+
40
+
41
+ def _try_wsl_fallback(
42
+ state: str, reason: str, pulseaudio_state: str | None = None
43
+ ) -> dict:
44
+ """Try WSL Windows fallback, return appropriate result dict."""
45
+ wsl_check = check_wsl_windows_audio_available()
46
+ if wsl_check["available"]:
47
+ result = {
48
+ "available": True,
49
+ "state": "WSL_WINDOWS",
50
+ "reason": wsl_check["reason"],
51
+ "fallback": "windows_powershell",
52
+ }
53
+ if pulseaudio_state:
54
+ result["pulseaudio_state"] = pulseaudio_state
55
+ return result
56
+ return {"available": False, "state": state, "reason": reason}
57
+
58
+
59
+ def _parse_pulseaudio_state(output: str) -> dict:
60
+ """Parse PulseAudio sink state from pactl output."""
61
+ for line in output.strip().split("\n"):
62
+ parts = line.split("\t")
63
+ if len(parts) >= 5:
64
+ state = parts[4]
65
+ if state == "SUSPENDED":
66
+ return _try_wsl_fallback(
67
+ "SUSPENDED",
68
+ "Audio sink SUSPENDED (no active output device)",
69
+ pulseaudio_state="SUSPENDED",
70
+ )
71
+ if state in ("RUNNING", "IDLE"):
72
+ return {
73
+ "available": True,
74
+ "state": state,
75
+ "reason": f"Audio sink is {state}",
76
+ }
77
+
78
+ return _try_wsl_fallback("UNKNOWN", "Could not determine sink state")
13
79
 
14
80
 
15
81
  def check_local_audio_available() -> dict:
@@ -18,11 +84,15 @@ def check_local_audio_available() -> dict:
18
84
  Checks PulseAudio sink state to determine if audio can actually be heard.
19
85
  On NAS or headless servers, the sink is typically SUSPENDED.
20
86
 
21
- Returns:
87
+ In WSL environments, also checks for Windows playback fallback via PowerShell.
88
+
89
+ Returns
90
+ -------
22
91
  dict with keys:
23
92
  - available: bool - True if local audio output is likely to work
24
93
  - state: str - 'RUNNING', 'IDLE', 'SUSPENDED', 'NO_SINK', etc.
25
94
  - reason: str - Human-readable explanation
95
+ - fallback: str (optional) - Fallback method if primary unavailable
26
96
  """
27
97
  try:
28
98
  result = subprocess.run(
@@ -32,45 +102,22 @@ def check_local_audio_available() -> dict:
32
102
  timeout=5,
33
103
  )
34
104
  if result.returncode != 0:
35
- return {
36
- "available": False,
37
- "state": "NO_PACTL",
38
- "reason": "PulseAudio not available",
39
- }
105
+ return _try_wsl_fallback("NO_PACTL", "PulseAudio not available")
40
106
 
41
107
  if not result.stdout.strip():
42
- return {
43
- "available": False,
44
- "state": "NO_SINK",
45
- "reason": "No audio sinks found",
46
- }
108
+ return _try_wsl_fallback("NO_SINK", "No audio sinks found")
47
109
 
48
- # Parse sink state (format: id\tname\tmodule\tformat\tstate)
49
- for line in result.stdout.strip().split("\n"):
50
- parts = line.split("\t")
51
- if len(parts) >= 5:
52
- state = parts[4]
53
- if state == "SUSPENDED":
54
- return {
55
- "available": False,
56
- "state": "SUSPENDED",
57
- "reason": "Audio sink SUSPENDED (no active output device)",
58
- }
59
- elif state in ("RUNNING", "IDLE"):
60
- return {
61
- "available": True,
62
- "state": state,
63
- "reason": f"Audio sink is {state}",
64
- }
65
-
66
- return {
67
- "available": False,
68
- "state": "UNKNOWN",
69
- "reason": "Could not determine sink state",
70
- }
110
+ return _parse_pulseaudio_state(result.stdout)
71
111
 
72
112
  except FileNotFoundError:
73
- # No pactl - might be macOS or minimal system, assume available
113
+ wsl_check = check_wsl_windows_audio_available()
114
+ if wsl_check["available"]:
115
+ return {
116
+ "available": True,
117
+ "state": "WSL_WINDOWS",
118
+ "reason": wsl_check["reason"],
119
+ "fallback": "windows_powershell",
120
+ }
74
121
  return {
75
122
  "available": True,
76
123
  "state": "NO_PACTL",
@@ -83,11 +130,7 @@ def check_local_audio_available() -> dict:
83
130
  "reason": "PulseAudio query timed out",
84
131
  }
85
132
  except Exception as e:
86
- return {
87
- "available": False,
88
- "state": "ERROR",
89
- "reason": str(e),
90
- }
133
+ return {"available": False, "state": "ERROR", "reason": str(e)}
91
134
 
92
135
 
93
136
  # EOF
scitex/cli/capture.py CHANGED
@@ -406,32 +406,55 @@ def doctor():
406
406
 
407
407
 
408
408
  @mcp.command("list-tools")
409
- def list_tools():
410
- """
411
- List available MCP tools
412
-
413
- \b
414
- Example:
415
- scitex capture mcp list-tools
416
- """
409
+ @click.option("-v", "--verbose", count=True, help="-v params, -vv returns")
410
+ def list_tools(verbose):
411
+ """List available MCP tools for capture."""
417
412
  click.secho("Capture MCP Tools", fg="cyan", bold=True)
418
413
  click.echo()
414
+ # (name, desc, params, returns)
419
415
  tools = [
420
- ("capture_capture_screenshot", "Capture screenshot"),
421
- ("capture_capture_window", "Capture specific window"),
422
- ("capture_start_monitoring", "Start continuous capture"),
423
- ("capture_stop_monitoring", "Stop monitoring"),
424
- ("capture_get_monitoring_status", "Get monitoring status"),
425
- ("capture_analyze_screenshot", "Analyze screenshot for errors"),
426
- ("capture_list_recent_screenshots", "List recent screenshots"),
427
- ("capture_clear_cache", "Clear screenshot cache"),
428
- ("capture_create_gif", "Create animated GIF"),
429
- ("capture_list_sessions", "List monitoring sessions"),
430
- ("capture_get_info", "Get monitor/window info"),
431
- ("capture_list_windows", "List visible windows"),
416
+ (
417
+ "capture_screenshot",
418
+ "Capture screenshot",
419
+ "output_path=None, monitor=0",
420
+ "str",
421
+ ),
422
+ (
423
+ "capture_window",
424
+ "Capture specific window",
425
+ "window_id: str, output=None",
426
+ "str",
427
+ ),
428
+ (
429
+ "start_monitoring",
430
+ "Start continuous capture",
431
+ "interval=5.0, monitor=0",
432
+ "str",
433
+ ),
434
+ ("stop_monitoring", "Stop monitoring", "session_id=None", "str"),
435
+ ("get_monitoring_status", "Get monitoring status", "", "JSON"),
436
+ (
437
+ "analyze_screenshot",
438
+ "Analyze screenshot for errors",
439
+ "image_path: str",
440
+ "JSON",
441
+ ),
442
+ ("list_recent_screenshots", "List recent screenshots", "limit=10", "JSON"),
443
+ ("clear_cache", "Clear screenshot cache", "older_than_hours=24", "JSON"),
444
+ ("create_gif", "Create animated GIF", "session_id: str, output=None", "str"),
445
+ ("list_sessions", "List monitoring sessions", "", "JSON"),
446
+ ("get_info", "Get monitor/window info", "", "JSON"),
447
+ ("list_windows", "List visible windows", "", "JSON"),
432
448
  ]
433
- for name, desc in tools:
434
- click.echo(f" {name}: {desc}")
449
+ for name, desc, params, returns in tools:
450
+ click.secho(f" capture_{name}", fg="green", bold=True, nl=False)
451
+ click.echo(f": {desc}")
452
+ if verbose >= 1 and params:
453
+ click.echo(f" params: {params}")
454
+ if verbose >= 2:
455
+ click.echo(f" returns: {returns}")
456
+ if verbose >= 1:
457
+ click.echo()
435
458
 
436
459
 
437
460
  @capture.command("list-python-apis")