scitex 2.16.2__py3-none-any.whl → 2.17.0__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.
- scitex/_mcp_resources/_cheatsheet.py +1 -1
- scitex/_mcp_resources/_modules.py +1 -1
- scitex/_mcp_tools/__init__.py +2 -0
- scitex/_mcp_tools/verify.py +256 -0
- scitex/cli/main.py +2 -0
- scitex/cli/verify.py +476 -0
- scitex/dev/plt/__init__.py +1 -1
- scitex/dev/plt/data/mpl/PLOTTING_FUNCTIONS.yaml +90 -0
- scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES.yaml +1571 -0
- scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES_DETAILED.yaml +6262 -0
- scitex/dev/plt/data/mpl/SIGNATURES_FLATTENED.yaml +1274 -0
- scitex/dev/plt/data/mpl/dir_ax.txt +459 -0
- scitex/dev/plt/mpl/get_dir_ax.py +1 -1
- scitex/dev/plt/mpl/get_signatures.py +1 -1
- scitex/dev/plt/mpl/get_signatures_details.py +1 -1
- scitex/io/_load.py +8 -1
- scitex/io/_save.py +12 -0
- scitex/scholar/data/.gitkeep +0 -0
- scitex/scholar/data/README.md +44 -0
- scitex/scholar/data/bib_files/bibliography.bib +1952 -0
- scitex/scholar/data/bib_files/neurovista.bib +277 -0
- scitex/scholar/data/bib_files/neurovista_enriched.bib +441 -0
- scitex/scholar/data/bib_files/neurovista_enriched_enriched.bib +441 -0
- scitex/scholar/data/bib_files/neurovista_processed.bib +338 -0
- scitex/scholar/data/bib_files/openaccess.bib +89 -0
- scitex/scholar/data/bib_files/pac-seizure_prediction_enriched.bib +2178 -0
- scitex/scholar/data/bib_files/pac.bib +698 -0
- scitex/scholar/data/bib_files/pac_enriched.bib +1061 -0
- scitex/scholar/data/bib_files/pac_processed.bib +0 -0
- scitex/scholar/data/bib_files/pac_titles.txt +75 -0
- scitex/scholar/data/bib_files/paywalled.bib +98 -0
- scitex/scholar/data/bib_files/related-papers-by-coauthors.bib +58 -0
- scitex/scholar/data/bib_files/related-papers-by-coauthors_enriched.bib +87 -0
- scitex/scholar/data/bib_files/seizure_prediction.bib +694 -0
- scitex/scholar/data/bib_files/seizure_prediction_processed.bib +0 -0
- scitex/scholar/data/bib_files/test_complete_enriched.bib +437 -0
- scitex/scholar/data/bib_files/test_final_enriched.bib +437 -0
- scitex/scholar/data/bib_files/test_seizure.bib +46 -0
- scitex/scholar/data/impact_factor/JCR_IF_2022.xlsx +0 -0
- scitex/scholar/data/impact_factor/JCR_IF_2024.db +0 -0
- scitex/scholar/data/impact_factor/JCR_IF_2024.xlsx +0 -0
- scitex/scholar/data/impact_factor/JCR_IF_2024_v01.db +0 -0
- scitex/scholar/data/impact_factor.db +0 -0
- scitex/session/README.md +2 -2
- scitex/session/__init__.py +1 -0
- scitex/session/_decorator.py +57 -33
- scitex/session/_lifecycle/__init__.py +23 -0
- scitex/session/_lifecycle/_close.py +225 -0
- scitex/session/_lifecycle/_config.py +112 -0
- scitex/session/_lifecycle/_matplotlib.py +83 -0
- scitex/session/_lifecycle/_start.py +246 -0
- scitex/session/_lifecycle/_utils.py +186 -0
- scitex/session/_manager.py +40 -3
- scitex/session/template.py +1 -1
- scitex/template/_templates/plt.py +1 -1
- scitex/template/_templates/session.py +1 -1
- scitex/verify/README.md +312 -0
- scitex/verify/__init__.py +212 -0
- scitex/verify/_chain.py +369 -0
- scitex/verify/_db.py +600 -0
- scitex/verify/_hash.py +187 -0
- scitex/verify/_integration.py +127 -0
- scitex/verify/_rerun.py +253 -0
- scitex/verify/_tracker.py +330 -0
- scitex/verify/_visualize.py +48 -0
- scitex/verify/_viz/__init__.py +56 -0
- scitex/verify/_viz/_colors.py +84 -0
- scitex/verify/_viz/_format.py +302 -0
- scitex/verify/_viz/_json.py +192 -0
- scitex/verify/_viz/_mermaid.py +440 -0
- scitex/verify/_viz/_plotly.py +193 -0
- scitex/verify/_viz/_templates.py +246 -0
- scitex/verify/_viz/_utils.py +56 -0
- {scitex-2.16.2.dist-info → scitex-2.17.0.dist-info}/METADATA +1 -1
- {scitex-2.16.2.dist-info → scitex-2.17.0.dist-info}/RECORD +78 -29
- scitex/scholar/url_finder/.tmp/open_url/KNOWN_RESOLVERS.py +0 -462
- scitex/scholar/url_finder/.tmp/open_url/README.md +0 -223
- scitex/scholar/url_finder/.tmp/open_url/_DOIToURLResolver.py +0 -694
- scitex/scholar/url_finder/.tmp/open_url/_OpenURLResolver.py +0 -1160
- scitex/scholar/url_finder/.tmp/open_url/_ResolverLinkFinder.py +0 -344
- scitex/scholar/url_finder/.tmp/open_url/__init__.py +0 -24
- scitex/session/_lifecycle.py +0 -827
- {scitex-2.16.2.dist-info → scitex-2.17.0.dist-info}/WHEEL +0 -0
- {scitex-2.16.2.dist-info → scitex-2.17.0.dist-info}/entry_points.txt +0 -0
- {scitex-2.16.2.dist-info → scitex-2.17.0.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
|
scitex/verify/_rerun.py
ADDED
|
@@ -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
|