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/_chain.py
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2026-02-01 (ywatanabe)"
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-python/src/scitex/verify/_chain.py
|
|
4
|
+
"""Dependency chain tracking and verification."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, List, Optional, Union
|
|
12
|
+
|
|
13
|
+
from ._db import get_db
|
|
14
|
+
from ._hash import hash_file
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class VerificationStatus(Enum):
|
|
18
|
+
"""Verification status for a run or file."""
|
|
19
|
+
|
|
20
|
+
VERIFIED = "verified"
|
|
21
|
+
MISMATCH = "mismatch"
|
|
22
|
+
MISSING = "missing"
|
|
23
|
+
UNKNOWN = "unknown"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class VerificationLevel(Enum):
|
|
27
|
+
"""Level of verification performed."""
|
|
28
|
+
|
|
29
|
+
CACHE = "cache" # Hash comparison only (fast)
|
|
30
|
+
RERUN = "rerun" # Full re-execution (thorough)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class FileVerification:
|
|
35
|
+
"""Verification result for a single file."""
|
|
36
|
+
|
|
37
|
+
path: str
|
|
38
|
+
role: str
|
|
39
|
+
expected_hash: str
|
|
40
|
+
current_hash: Optional[str]
|
|
41
|
+
status: VerificationStatus
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def is_verified(self) -> bool:
|
|
45
|
+
return self.status == VerificationStatus.VERIFIED
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class RunVerification:
|
|
50
|
+
"""Verification result for a session run."""
|
|
51
|
+
|
|
52
|
+
session_id: str
|
|
53
|
+
script_path: Optional[str]
|
|
54
|
+
status: VerificationStatus
|
|
55
|
+
files: List[FileVerification]
|
|
56
|
+
combined_hash_expected: Optional[str]
|
|
57
|
+
combined_hash_current: Optional[str]
|
|
58
|
+
level: VerificationLevel = VerificationLevel.CACHE
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def is_verified(self) -> bool:
|
|
62
|
+
return self.status == VerificationStatus.VERIFIED
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def is_verified_from_scratch(self) -> bool:
|
|
66
|
+
return self.is_verified and self.level == VerificationLevel.RERUN
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def inputs(self) -> List[FileVerification]:
|
|
70
|
+
return [f for f in self.files if f.role == "input"]
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def outputs(self) -> List[FileVerification]:
|
|
74
|
+
return [f for f in self.files if f.role == "output"]
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def mismatched_files(self) -> List[FileVerification]:
|
|
78
|
+
return [f for f in self.files if f.status == VerificationStatus.MISMATCH]
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def missing_files(self) -> List[FileVerification]:
|
|
82
|
+
return [f for f in self.files if f.status == VerificationStatus.MISSING]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class ChainVerification:
|
|
87
|
+
"""Verification result for a dependency chain."""
|
|
88
|
+
|
|
89
|
+
target_file: str
|
|
90
|
+
runs: List[RunVerification]
|
|
91
|
+
status: VerificationStatus
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def is_verified(self) -> bool:
|
|
95
|
+
return self.status == VerificationStatus.VERIFIED
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def failed_runs(self) -> List[RunVerification]:
|
|
99
|
+
return [r for r in self.runs if not r.is_verified]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def verify_file(
|
|
103
|
+
path: Union[str, Path],
|
|
104
|
+
expected_hash: str,
|
|
105
|
+
role: str = "unknown",
|
|
106
|
+
) -> FileVerification:
|
|
107
|
+
"""
|
|
108
|
+
Verify a single file against expected hash.
|
|
109
|
+
|
|
110
|
+
Parameters
|
|
111
|
+
----------
|
|
112
|
+
path : str or Path
|
|
113
|
+
Path to the file
|
|
114
|
+
expected_hash : str
|
|
115
|
+
Expected hash value
|
|
116
|
+
role : str, optional
|
|
117
|
+
Role of the file (input, output, script)
|
|
118
|
+
|
|
119
|
+
Returns
|
|
120
|
+
-------
|
|
121
|
+
FileVerification
|
|
122
|
+
Verification result
|
|
123
|
+
"""
|
|
124
|
+
path = Path(path)
|
|
125
|
+
path_str = str(path)
|
|
126
|
+
|
|
127
|
+
if not path.exists():
|
|
128
|
+
return FileVerification(
|
|
129
|
+
path=path_str,
|
|
130
|
+
role=role,
|
|
131
|
+
expected_hash=expected_hash,
|
|
132
|
+
current_hash=None,
|
|
133
|
+
status=VerificationStatus.MISSING,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
current_hash = hash_file(path)
|
|
137
|
+
|
|
138
|
+
# Compare only the length of expected_hash
|
|
139
|
+
matches = current_hash[: len(expected_hash)] == expected_hash
|
|
140
|
+
|
|
141
|
+
return FileVerification(
|
|
142
|
+
path=path_str,
|
|
143
|
+
role=role,
|
|
144
|
+
expected_hash=expected_hash,
|
|
145
|
+
current_hash=current_hash,
|
|
146
|
+
status=VerificationStatus.VERIFIED if matches else VerificationStatus.MISMATCH,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def verify_run(
|
|
151
|
+
target: str,
|
|
152
|
+
propagate: bool = True,
|
|
153
|
+
) -> RunVerification:
|
|
154
|
+
"""
|
|
155
|
+
Verify a session run by checking all file hashes.
|
|
156
|
+
|
|
157
|
+
Parameters
|
|
158
|
+
----------
|
|
159
|
+
target : str
|
|
160
|
+
Session ID, script path, or artifact path
|
|
161
|
+
propagate : bool
|
|
162
|
+
If True, mark as failed if any upstream input has failed verification
|
|
163
|
+
|
|
164
|
+
Returns
|
|
165
|
+
-------
|
|
166
|
+
RunVerification
|
|
167
|
+
Verification result
|
|
168
|
+
"""
|
|
169
|
+
db = get_db()
|
|
170
|
+
|
|
171
|
+
# Resolve target to session_id
|
|
172
|
+
session_id = _resolve_target(db, target)
|
|
173
|
+
if not session_id:
|
|
174
|
+
return RunVerification(
|
|
175
|
+
session_id=target,
|
|
176
|
+
script_path=None,
|
|
177
|
+
status=VerificationStatus.UNKNOWN,
|
|
178
|
+
files=[],
|
|
179
|
+
combined_hash_expected=None,
|
|
180
|
+
combined_hash_current=None,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Get run info
|
|
184
|
+
run_info = db.get_run(session_id)
|
|
185
|
+
if not run_info:
|
|
186
|
+
return RunVerification(
|
|
187
|
+
session_id=session_id,
|
|
188
|
+
script_path=None,
|
|
189
|
+
status=VerificationStatus.UNKNOWN,
|
|
190
|
+
files=[],
|
|
191
|
+
combined_hash_expected=None,
|
|
192
|
+
combined_hash_current=None,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Get all file hashes
|
|
196
|
+
input_hashes = db.get_file_hashes(session_id, role="input")
|
|
197
|
+
output_hashes = db.get_file_hashes(session_id, role="output")
|
|
198
|
+
|
|
199
|
+
# Verify each file
|
|
200
|
+
file_verifications = []
|
|
201
|
+
upstream_failed = False
|
|
202
|
+
|
|
203
|
+
for path, expected in input_hashes.items():
|
|
204
|
+
fv = verify_file(path, expected, role="input")
|
|
205
|
+
file_verifications.append(fv)
|
|
206
|
+
|
|
207
|
+
# Check if upstream session that produced this input has failed
|
|
208
|
+
if propagate and not fv.is_verified:
|
|
209
|
+
upstream_failed = True
|
|
210
|
+
|
|
211
|
+
for path, expected in output_hashes.items():
|
|
212
|
+
file_verifications.append(verify_file(path, expected, role="output"))
|
|
213
|
+
|
|
214
|
+
# Verify script if present
|
|
215
|
+
if run_info.get("script_path") and run_info.get("script_hash"):
|
|
216
|
+
script_verification = verify_file(
|
|
217
|
+
run_info["script_path"],
|
|
218
|
+
run_info["script_hash"],
|
|
219
|
+
role="script",
|
|
220
|
+
)
|
|
221
|
+
file_verifications.append(script_verification)
|
|
222
|
+
|
|
223
|
+
# Determine overall status (upstream failure propagates)
|
|
224
|
+
if upstream_failed:
|
|
225
|
+
status = VerificationStatus.MISMATCH
|
|
226
|
+
elif all(f.is_verified for f in file_verifications):
|
|
227
|
+
status = VerificationStatus.VERIFIED
|
|
228
|
+
elif any(f.status == VerificationStatus.MISMATCH for f in file_verifications):
|
|
229
|
+
status = VerificationStatus.MISMATCH
|
|
230
|
+
elif any(f.status == VerificationStatus.MISSING for f in file_verifications):
|
|
231
|
+
status = VerificationStatus.MISSING
|
|
232
|
+
else:
|
|
233
|
+
status = VerificationStatus.UNKNOWN
|
|
234
|
+
|
|
235
|
+
return RunVerification(
|
|
236
|
+
session_id=session_id,
|
|
237
|
+
script_path=run_info.get("script_path"),
|
|
238
|
+
status=status,
|
|
239
|
+
files=file_verifications,
|
|
240
|
+
combined_hash_expected=run_info.get("combined_hash"),
|
|
241
|
+
combined_hash_current=None,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _resolve_target(db, target: str) -> str | None:
|
|
246
|
+
"""Resolve target (session_id, script path, or artifact path) to session_id."""
|
|
247
|
+
# Try as session_id
|
|
248
|
+
if db.get_run(target):
|
|
249
|
+
return target
|
|
250
|
+
|
|
251
|
+
# Resolve to absolute path
|
|
252
|
+
resolved = str(Path(target).resolve())
|
|
253
|
+
|
|
254
|
+
# Try as script path
|
|
255
|
+
for run in db.list_runs(limit=100):
|
|
256
|
+
if run.get("script_path") == resolved:
|
|
257
|
+
return run["session_id"]
|
|
258
|
+
|
|
259
|
+
# Try as artifact (output) path
|
|
260
|
+
sessions = db.find_session_by_file(resolved, role="output")
|
|
261
|
+
return sessions[0] if sessions else None
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def verify_chain(
|
|
265
|
+
target: Union[str, Path],
|
|
266
|
+
) -> ChainVerification:
|
|
267
|
+
"""
|
|
268
|
+
Verify the dependency chain for a target file.
|
|
269
|
+
|
|
270
|
+
Traces back through all sessions that produced this file
|
|
271
|
+
and verifies each one.
|
|
272
|
+
|
|
273
|
+
Parameters
|
|
274
|
+
----------
|
|
275
|
+
target : str or Path
|
|
276
|
+
Target file to trace
|
|
277
|
+
|
|
278
|
+
Returns
|
|
279
|
+
-------
|
|
280
|
+
ChainVerification
|
|
281
|
+
Verification result for the entire chain
|
|
282
|
+
"""
|
|
283
|
+
db = get_db()
|
|
284
|
+
target = str(Path(target).resolve())
|
|
285
|
+
|
|
286
|
+
# Find session that produced this output
|
|
287
|
+
sessions = db.find_session_by_file(target, role="output")
|
|
288
|
+
if not sessions:
|
|
289
|
+
return ChainVerification(
|
|
290
|
+
target_file=target,
|
|
291
|
+
runs=[],
|
|
292
|
+
status=VerificationStatus.UNKNOWN,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# Get the most recent session
|
|
296
|
+
session_id = sessions[0]
|
|
297
|
+
|
|
298
|
+
# Build chain by following parent_session links
|
|
299
|
+
chain = db.get_chain(session_id)
|
|
300
|
+
|
|
301
|
+
# Verify each run in the chain
|
|
302
|
+
run_verifications = []
|
|
303
|
+
for sid in chain:
|
|
304
|
+
run_verifications.append(verify_run(sid))
|
|
305
|
+
|
|
306
|
+
# Determine overall status
|
|
307
|
+
if all(r.is_verified for r in run_verifications):
|
|
308
|
+
status = VerificationStatus.VERIFIED
|
|
309
|
+
elif any(r.status == VerificationStatus.MISMATCH for r in run_verifications):
|
|
310
|
+
status = VerificationStatus.MISMATCH
|
|
311
|
+
elif any(r.status == VerificationStatus.MISSING for r in run_verifications):
|
|
312
|
+
status = VerificationStatus.MISSING
|
|
313
|
+
else:
|
|
314
|
+
status = VerificationStatus.UNKNOWN
|
|
315
|
+
|
|
316
|
+
return ChainVerification(
|
|
317
|
+
target_file=target,
|
|
318
|
+
runs=run_verifications,
|
|
319
|
+
status=status,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def get_status() -> Dict[str, Any]:
|
|
324
|
+
"""
|
|
325
|
+
Get verification status for all runs (like git status).
|
|
326
|
+
|
|
327
|
+
Returns
|
|
328
|
+
-------
|
|
329
|
+
dict
|
|
330
|
+
Summary of verification status
|
|
331
|
+
"""
|
|
332
|
+
db = get_db()
|
|
333
|
+
runs = db.list_runs(limit=1000)
|
|
334
|
+
|
|
335
|
+
verified = []
|
|
336
|
+
mismatched = []
|
|
337
|
+
missing = []
|
|
338
|
+
|
|
339
|
+
for run in runs:
|
|
340
|
+
session_id = run["session_id"]
|
|
341
|
+
verification = verify_run(session_id)
|
|
342
|
+
|
|
343
|
+
if verification.is_verified:
|
|
344
|
+
verified.append(session_id)
|
|
345
|
+
elif verification.mismatched_files:
|
|
346
|
+
mismatched.append(
|
|
347
|
+
{
|
|
348
|
+
"session_id": session_id,
|
|
349
|
+
"files": [f.path for f in verification.mismatched_files],
|
|
350
|
+
}
|
|
351
|
+
)
|
|
352
|
+
elif verification.missing_files:
|
|
353
|
+
missing.append(
|
|
354
|
+
{
|
|
355
|
+
"session_id": session_id,
|
|
356
|
+
"files": [f.path for f in verification.missing_files],
|
|
357
|
+
}
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
"verified_count": len(verified),
|
|
362
|
+
"mismatch_count": len(mismatched),
|
|
363
|
+
"missing_count": len(missing),
|
|
364
|
+
"mismatched": mismatched,
|
|
365
|
+
"missing": missing,
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
# EOF
|