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
scitex/verify/_hash.py ADDED
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-02-01 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/verify/_hash.py
4
+ """File and directory hashing utilities for verification."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import hashlib
9
+ from pathlib import Path
10
+ from typing import Dict, Union
11
+
12
+
13
+ def hash_file(
14
+ path: Union[str, Path],
15
+ algorithm: str = "sha256",
16
+ chunk_size: int = 8192,
17
+ ) -> str:
18
+ """
19
+ Compute hash of a file.
20
+
21
+ Parameters
22
+ ----------
23
+ path : str or Path
24
+ Path to the file to hash
25
+ algorithm : str, optional
26
+ Hash algorithm (default: sha256)
27
+ chunk_size : int, optional
28
+ Size of chunks to read (default: 8192)
29
+
30
+ Returns
31
+ -------
32
+ str
33
+ Hexadecimal hash string (first 32 characters)
34
+
35
+ Examples
36
+ --------
37
+ >>> hash_file("data.csv")
38
+ 'a1b2c3d4e5f6...'
39
+ """
40
+ path = Path(path)
41
+ if not path.exists():
42
+ raise FileNotFoundError(f"File not found: {path}")
43
+
44
+ hasher = hashlib.new(algorithm)
45
+ with open(path, "rb") as f:
46
+ while chunk := f.read(chunk_size):
47
+ hasher.update(chunk)
48
+
49
+ return hasher.hexdigest()[:32]
50
+
51
+
52
+ def hash_directory(
53
+ path: Union[str, Path],
54
+ pattern: str = "*",
55
+ recursive: bool = True,
56
+ algorithm: str = "sha256",
57
+ ) -> Dict[str, str]:
58
+ """
59
+ Compute hashes for all files in a directory.
60
+
61
+ Parameters
62
+ ----------
63
+ path : str or Path
64
+ Directory path
65
+ pattern : str, optional
66
+ Glob pattern for files (default: "*")
67
+ recursive : bool, optional
68
+ Whether to search recursively (default: True)
69
+ algorithm : str, optional
70
+ Hash algorithm (default: sha256)
71
+
72
+ Returns
73
+ -------
74
+ dict
75
+ Mapping of relative paths to hashes
76
+
77
+ Examples
78
+ --------
79
+ >>> hash_directory("./data/")
80
+ {'input.csv': 'a1b2...', 'config.yaml': 'c3d4...'}
81
+ """
82
+ path = Path(path)
83
+ if not path.is_dir():
84
+ raise NotADirectoryError(f"Not a directory: {path}")
85
+
86
+ glob_method = path.rglob if recursive else path.glob
87
+ hashes = {}
88
+
89
+ for file_path in glob_method(pattern):
90
+ if file_path.is_file():
91
+ rel_path = str(file_path.relative_to(path))
92
+ hashes[rel_path] = hash_file(file_path, algorithm=algorithm)
93
+
94
+ return hashes
95
+
96
+
97
+ def hash_files(
98
+ paths: list[Union[str, Path]],
99
+ algorithm: str = "sha256",
100
+ ) -> Dict[str, str]:
101
+ """
102
+ Compute hashes for a list of files.
103
+
104
+ Parameters
105
+ ----------
106
+ paths : list of str or Path
107
+ List of file paths
108
+ algorithm : str, optional
109
+ Hash algorithm (default: sha256)
110
+
111
+ Returns
112
+ -------
113
+ dict
114
+ Mapping of paths to hashes
115
+ """
116
+ hashes = {}
117
+ for path in paths:
118
+ path = Path(path)
119
+ if path.exists() and path.is_file():
120
+ hashes[str(path)] = hash_file(path, algorithm=algorithm)
121
+ return hashes
122
+
123
+
124
+ def combine_hashes(hashes: Dict[str, str], algorithm: str = "sha256") -> str:
125
+ """
126
+ Combine multiple hashes into a single hash.
127
+
128
+ Creates a deterministic combined hash from a dictionary of hashes.
129
+
130
+ Parameters
131
+ ----------
132
+ hashes : dict
133
+ Mapping of names to hashes
134
+ algorithm : str, optional
135
+ Hash algorithm (default: sha256)
136
+
137
+ Returns
138
+ -------
139
+ str
140
+ Combined hash (first 32 characters)
141
+
142
+ Examples
143
+ --------
144
+ >>> hashes = {'input.csv': 'a1b2...', 'script.py': 'c3d4...'}
145
+ >>> combine_hashes(hashes)
146
+ 'e5f6g7h8...'
147
+ """
148
+ hasher = hashlib.new(algorithm)
149
+
150
+ # Sort by key for deterministic ordering
151
+ for key in sorted(hashes.keys()):
152
+ hasher.update(f"{key}:{hashes[key]}".encode())
153
+
154
+ return hasher.hexdigest()[:32]
155
+
156
+
157
+ def verify_hash(
158
+ path: Union[str, Path],
159
+ expected_hash: str,
160
+ algorithm: str = "sha256",
161
+ ) -> bool:
162
+ """
163
+ Verify that a file matches an expected hash.
164
+
165
+ Parameters
166
+ ----------
167
+ path : str or Path
168
+ Path to the file
169
+ expected_hash : str
170
+ Expected hash value
171
+ algorithm : str, optional
172
+ Hash algorithm (default: sha256)
173
+
174
+ Returns
175
+ -------
176
+ bool
177
+ True if hash matches, False otherwise
178
+ """
179
+ try:
180
+ actual_hash = hash_file(path, algorithm=algorithm)
181
+ # Compare only the length of expected_hash (may be truncated)
182
+ return actual_hash[: len(expected_hash)] == expected_hash
183
+ except FileNotFoundError:
184
+ return False
185
+
186
+
187
+ # EOF
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-02-01 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/verify/_integration.py
4
+ """Integration hooks for session and io modules."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+ from typing import Optional, Union
10
+
11
+ from ._tracker import get_tracker, start_tracking, stop_tracking
12
+
13
+
14
+ def on_session_start(
15
+ session_id: str,
16
+ script_path: Optional[str] = None,
17
+ parent_session: Optional[str] = None,
18
+ verbose: bool = False,
19
+ ) -> None:
20
+ """
21
+ Hook called when a session starts.
22
+
23
+ Parameters
24
+ ----------
25
+ session_id : str
26
+ Unique session identifier
27
+ script_path : str, optional
28
+ Path to the script being run
29
+ parent_session : str, optional
30
+ Parent session ID for chain tracking
31
+ verbose : bool, optional
32
+ Whether to log status messages
33
+ """
34
+ try:
35
+ start_tracking(
36
+ session_id=session_id,
37
+ script_path=script_path,
38
+ parent_session=parent_session,
39
+ )
40
+ except Exception as e:
41
+ if verbose:
42
+ import logging
43
+
44
+ logging.getLogger(__name__).warning(
45
+ f"Could not start verification tracking: {e}"
46
+ )
47
+
48
+
49
+ def on_session_close(
50
+ status: str = "success",
51
+ exit_code: int = 0,
52
+ verbose: bool = False,
53
+ ) -> None:
54
+ """
55
+ Hook called when a session closes.
56
+
57
+ Parameters
58
+ ----------
59
+ status : str, optional
60
+ Final status (success, failed, error)
61
+ exit_code : int, optional
62
+ Exit code of the script
63
+ verbose : bool, optional
64
+ Whether to log status messages
65
+ """
66
+ try:
67
+ stop_tracking(status=status, exit_code=exit_code)
68
+ except Exception as e:
69
+ if verbose:
70
+ import logging
71
+
72
+ logging.getLogger(__name__).warning(
73
+ f"Could not stop verification tracking: {e}"
74
+ )
75
+
76
+
77
+ def on_io_load(
78
+ path: Union[str, Path],
79
+ track: bool = True,
80
+ ) -> None:
81
+ """
82
+ Hook called when a file is loaded via stx.io.load().
83
+
84
+ Parameters
85
+ ----------
86
+ path : str or Path
87
+ Path to the loaded file
88
+ track : bool, optional
89
+ Whether to track this file as an input
90
+ """
91
+ if not track:
92
+ return
93
+
94
+ tracker = get_tracker()
95
+ if tracker is not None:
96
+ try:
97
+ tracker.record_input(path, track=track)
98
+ except Exception:
99
+ pass # Silent fail - don't interrupt io operations
100
+
101
+
102
+ def on_io_save(
103
+ path: Union[str, Path],
104
+ track: bool = True,
105
+ ) -> None:
106
+ """
107
+ Hook called when a file is saved via stx.io.save().
108
+
109
+ Parameters
110
+ ----------
111
+ path : str or Path
112
+ Path to the saved file
113
+ track : bool, optional
114
+ Whether to track this file as an output
115
+ """
116
+ if not track:
117
+ return
118
+
119
+ tracker = get_tracker()
120
+ if tracker is not None:
121
+ try:
122
+ tracker.record_output(path, track=track)
123
+ except Exception:
124
+ pass # Silent fail - don't interrupt io operations
125
+
126
+
127
+ # EOF
@@ -0,0 +1,253 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-02-01 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/verify/_rerun.py
4
+ """Rerun verification - re-execute scripts and compare outputs."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import shutil
9
+ import subprocess
10
+ from pathlib import Path
11
+ from typing import Dict
12
+
13
+ from ._chain import (
14
+ FileVerification,
15
+ RunVerification,
16
+ VerificationLevel,
17
+ VerificationStatus,
18
+ )
19
+ from ._db import get_db
20
+
21
+
22
+ def verify_by_rerun(
23
+ target: str | list[str],
24
+ timeout: int = 300,
25
+ cleanup: bool = True,
26
+ ) -> RunVerification | list[RunVerification]:
27
+ """
28
+ Verify session(s) by re-executing scripts and comparing outputs.
29
+
30
+ Parameters
31
+ ----------
32
+ target : str or list[str]
33
+ Session ID, script path, or artifact path.
34
+ - run_id: directly use this run
35
+ - script path: latest run that executed this script
36
+ - artifact path: latest run which produced this file
37
+ timeout : int, optional
38
+ Maximum execution time in seconds (default: 300)
39
+ cleanup : bool, optional
40
+ Whether to remove the new session's output directory after verification
41
+
42
+ Returns
43
+ -------
44
+ RunVerification or list[RunVerification]
45
+ Single result if single target, list if multiple targets
46
+ """
47
+ if isinstance(target, list):
48
+ return [_verify_single(t, timeout, cleanup) for t in target]
49
+ return _verify_single(target, timeout, cleanup)
50
+
51
+
52
+ def _verify_single(
53
+ target: str,
54
+ timeout: int = 300,
55
+ cleanup: bool = True,
56
+ ) -> RunVerification:
57
+ """Verify a single target."""
58
+ db = get_db()
59
+
60
+ # Resolve target to session_id
61
+ session_id = _resolve_to_session_id(db, target)
62
+ if not session_id:
63
+ return _unknown_result(target, None)
64
+
65
+ # Get original run info
66
+ run_info = db.get_run(session_id)
67
+ if not run_info:
68
+ return _unknown_result(session_id, None)
69
+
70
+ script_path = run_info.get("script_path")
71
+ if not script_path or not Path(script_path).exists():
72
+ return RunVerification(
73
+ session_id=session_id,
74
+ script_path=script_path,
75
+ status=VerificationStatus.MISSING,
76
+ files=[],
77
+ combined_hash_expected=None,
78
+ combined_hash_current=None,
79
+ level=VerificationLevel.RERUN,
80
+ )
81
+
82
+ # Get expected output hashes from original session
83
+ original_hashes = db.get_file_hashes(session_id, role="output")
84
+ if not original_hashes:
85
+ return _unknown_result(session_id, script_path)
86
+
87
+ # Re-execute the script (creates new session)
88
+ exec_result = _execute_script(script_path, timeout)
89
+ if exec_result is not None:
90
+ return exec_result._replace(session_id=session_id)
91
+
92
+ # Find the new session (most recent from this script)
93
+ new_session_id, new_sdir_run = _find_new_session(db, script_path, session_id)
94
+ if not new_session_id:
95
+ return _unknown_result(session_id, script_path)
96
+
97
+ # Get new session's output hashes
98
+ new_hashes = db.get_file_hashes(new_session_id, role="output")
99
+
100
+ # Compare hashes by filename
101
+ file_verifications = _compare_hashes(original_hashes, new_hashes)
102
+
103
+ # Cleanup new session's output directory if requested
104
+ if cleanup and new_sdir_run:
105
+ _cleanup_session_dir(new_sdir_run)
106
+
107
+ # Determine overall status
108
+ status = _determine_status(file_verifications)
109
+
110
+ # Record verification result in database for original session
111
+ db.record_verification(
112
+ session_id=session_id,
113
+ level=VerificationLevel.RERUN.value,
114
+ status=status.value,
115
+ )
116
+
117
+ return RunVerification(
118
+ session_id=session_id,
119
+ script_path=script_path,
120
+ status=status,
121
+ files=file_verifications,
122
+ combined_hash_expected=run_info.get("combined_hash"),
123
+ combined_hash_current=None,
124
+ level=VerificationLevel.RERUN,
125
+ )
126
+
127
+
128
+ def _resolve_to_session_id(db, target: str) -> str | None:
129
+ """Resolve target to session_id.
130
+
131
+ Accepts:
132
+ - run_id: directly use this run
133
+ - script path: latest run that executed this script
134
+ - artifact path: latest run which produced this file
135
+ """
136
+ # Try as run_id
137
+ if db.get_run(target):
138
+ return target
139
+
140
+ # Always resolve to absolute path
141
+ resolved = str(Path(target).resolve())
142
+
143
+ # Try as script path
144
+ for run in db.list_runs(limit=100):
145
+ if run.get("script_path") == resolved:
146
+ return run["session_id"]
147
+
148
+ # Try as artifact (output) path
149
+ sessions = db.find_session_by_file(resolved, role="output")
150
+ return sessions[0] if sessions else None
151
+
152
+
153
+ def _unknown_result(session_id: str, script_path: str) -> RunVerification:
154
+ """Create an unknown verification result."""
155
+ return RunVerification(
156
+ session_id=session_id,
157
+ script_path=script_path,
158
+ status=VerificationStatus.UNKNOWN,
159
+ files=[],
160
+ combined_hash_expected=None,
161
+ combined_hash_current=None,
162
+ level=VerificationLevel.RERUN,
163
+ )
164
+
165
+
166
+ def _execute_script(script_path: str, timeout: int) -> RunVerification | None:
167
+ """Execute script and return error result if failed, None if success."""
168
+ try:
169
+ result = subprocess.run(
170
+ ["python", script_path],
171
+ capture_output=True,
172
+ timeout=timeout,
173
+ cwd=Path(script_path).parent,
174
+ )
175
+ if result.returncode != 0:
176
+ return RunVerification(
177
+ session_id="",
178
+ script_path=script_path,
179
+ status=VerificationStatus.MISMATCH,
180
+ files=[],
181
+ combined_hash_expected=None,
182
+ combined_hash_current=None,
183
+ level=VerificationLevel.RERUN,
184
+ )
185
+ return None # Success
186
+ except subprocess.TimeoutExpired:
187
+ return _unknown_result("", script_path)
188
+ except Exception:
189
+ return _unknown_result("", script_path)
190
+
191
+
192
+ def _find_new_session(db, script_path: str, original_id: str) -> tuple:
193
+ """Find the new session created by re-running the script."""
194
+ recent_runs = db.list_runs(limit=5)
195
+ for run in recent_runs:
196
+ if run.get("script_path") == script_path and run["session_id"] != original_id:
197
+ return run["session_id"], run.get("sdir_run")
198
+ return None, None
199
+
200
+
201
+ def _compare_hashes(
202
+ original_hashes: Dict[str, str], new_hashes: Dict[str, str]
203
+ ) -> list:
204
+ """Compare hashes by filename and return FileVerification list."""
205
+ original_by_name = {Path(p).name: h for p, h in original_hashes.items()}
206
+ new_by_name = {Path(p).name: h for p, h in new_hashes.items()}
207
+
208
+ verifications = []
209
+ for filename, expected_hash in original_by_name.items():
210
+ current_hash = new_by_name.get(filename)
211
+ if current_hash is None:
212
+ status = VerificationStatus.MISSING
213
+ elif current_hash == expected_hash:
214
+ status = VerificationStatus.VERIFIED
215
+ else:
216
+ status = VerificationStatus.MISMATCH
217
+
218
+ verifications.append(
219
+ FileVerification(
220
+ path=filename,
221
+ role="output",
222
+ expected_hash=expected_hash,
223
+ current_hash=current_hash,
224
+ status=status,
225
+ )
226
+ )
227
+ return verifications
228
+
229
+
230
+ def _cleanup_session_dir(sdir_run: str) -> None:
231
+ """Remove the session's output directory (best-effort)."""
232
+ try:
233
+ path = Path(sdir_run)
234
+ if path.exists():
235
+ shutil.rmtree(path)
236
+ except Exception:
237
+ pass
238
+
239
+
240
+ def _determine_status(file_verifications: list) -> VerificationStatus:
241
+ """Determine overall verification status from file verifications."""
242
+ if all(f.is_verified for f in file_verifications):
243
+ return VerificationStatus.VERIFIED
244
+ if any(f.status == VerificationStatus.MISMATCH for f in file_verifications):
245
+ return VerificationStatus.MISMATCH
246
+ return VerificationStatus.UNKNOWN
247
+
248
+
249
+ # Backward compatibility alias
250
+ verify_run_from_scratch = verify_by_rerun
251
+
252
+
253
+ # EOF