seqevi 0.3.2__tar.gz → 0.3.4__tar.gz
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.
- {seqevi-0.3.2 → seqevi-0.3.4}/PKG-INFO +3 -3
- {seqevi-0.3.2 → seqevi-0.3.4}/README.md +2 -2
- {seqevi-0.3.2 → seqevi-0.3.4}/pyproject.toml +1 -1
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/__init__.py +1 -1
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/annotate.py +167 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/api.py +61 -2
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/cli.py +136 -1
- seqevi-0.3.4/src/seqevi/progress.py +88 -0
- seqevi-0.3.4/tests/test_api.py +250 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/tests/test_cli.py +116 -2
- seqevi-0.3.4/tests/test_progress.py +327 -0
- seqevi-0.3.2/tests/test_api.py +0 -91
- {seqevi-0.3.2 → seqevi-0.3.4}/LICENSE +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/__main__.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/adapters/__init__.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/adapters/base.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/adapters/dbcan_cazyme.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/adapters/eggnog.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/adapters/interpro_pfam.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/adapters/registry.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/distribution/__init__.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/distribution/manifest.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/distribution/oci.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/distribution/setup.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/errors.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/evidence.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/execution_profile.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/hashing.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/kits/__init__.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/kits/dbcan-cazyme.toml +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/resource_lock.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/result.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/runner.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/runtime_identity.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/sequence.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/service/__init__.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/service/app.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/service/config.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/service/persistence.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/store/__init__.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/store/_artifact_reader.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/store/_resolver.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/store/artifact.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/store/client.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/store/contract.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/store/factory.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/store/local.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/store/migration.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/store/migrations/__init__.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/store/migrations/env.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/store/migrations/versions/0001_initial_store.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/store/migrations/versions/0002_artifact_byte_size_bigint.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/store/migrations/versions/0003_evidence_claim_leases.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/store/migrations/versions/0004_claim_sessions.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/store/migrations/versions/__init__.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/store/schema.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/src/seqevi/store/transport.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/tests/__init__.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/tests/support.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/tests/test_acceptance_containment.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/tests/test_acceptance_harnesses.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/tests/test_annotate.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/tests/test_architecture_contracts.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/tests/test_artifact_store.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/tests/test_claim_leases.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/tests/test_dbcan_cazyme.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/tests/test_dbcan_image_assets.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/tests/test_dbcan_local_candidate_harness.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/tests/test_deployment_assets.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/tests/test_eggnog.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/tests/test_evidence.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/tests/test_execution_profile.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/tests/test_incremental_reuse.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/tests/test_interpro_pfam.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/tests/test_local_store.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/tests/test_managed_setup.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/tests/test_oci.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/tests/test_resource_lock.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/tests/test_result_prototype.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/tests/test_runner.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/tests/test_runtime_identity.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/tests/test_sequence.py +0 -0
- {seqevi-0.3.2 → seqevi-0.3.4}/tests/test_shared_store.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: seqevi
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: Content-addressed, reusable protein sequence annotation evidence.
|
|
5
5
|
Author-Email: Fuqing Zhang <fu.qing.zhang.work@gmail.com>, FuqingZhang <103730099+FuqingZh@users.noreply.github.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -45,11 +45,11 @@ single-file DuckDB result for the current FASTA.
|
|
|
45
45
|
## Status
|
|
46
46
|
|
|
47
47
|
The target architecture and v1.1 result contracts are approved. The SeqEvi
|
|
48
|
-
|
|
48
|
+
0.3.4 source tree uses strict protein sequence identity, the single-host
|
|
49
49
|
SQLite/POSIX Store, the external tool runner, exact cache-miss orchestration,
|
|
50
50
|
and atomic DuckDB result materialization.
|
|
51
51
|
|
|
52
|
-
SeqEvi 0.3.
|
|
52
|
+
SeqEvi 0.3.4 provides managed setup for dbCAN only. The `eggnog` and
|
|
53
53
|
`interpro-pfam` adapters remain supported through explicit runtimes and named
|
|
54
54
|
host profiles; managed setup for them is later feature work. The
|
|
55
55
|
[Slice D gate record](docs/benchmarks/20260806-v1.4-dbcan-public-release-gate.md)
|
|
@@ -11,11 +11,11 @@ single-file DuckDB result for the current FASTA.
|
|
|
11
11
|
## Status
|
|
12
12
|
|
|
13
13
|
The target architecture and v1.1 result contracts are approved. The SeqEvi
|
|
14
|
-
|
|
14
|
+
0.3.4 source tree uses strict protein sequence identity, the single-host
|
|
15
15
|
SQLite/POSIX Store, the external tool runner, exact cache-miss orchestration,
|
|
16
16
|
and atomic DuckDB result materialization.
|
|
17
17
|
|
|
18
|
-
SeqEvi 0.3.
|
|
18
|
+
SeqEvi 0.3.4 provides managed setup for dbCAN only. The `eggnog` and
|
|
19
19
|
`interpro-pfam` adapters remain supported through explicit runtimes and named
|
|
20
20
|
host profiles; managed setup for them is later feature work. The
|
|
21
21
|
[Slice D gate record](docs/benchmarks/20260806-v1.4-dbcan-public-release-gate.md)
|
|
@@ -26,6 +26,14 @@ from .evidence import (
|
|
|
26
26
|
EvidenceSource,
|
|
27
27
|
EvidenceStatus,
|
|
28
28
|
)
|
|
29
|
+
from .progress import (
|
|
30
|
+
ProgressEvent,
|
|
31
|
+
ProgressPhase,
|
|
32
|
+
ProgressSink,
|
|
33
|
+
ProgressState,
|
|
34
|
+
ProgressUnit,
|
|
35
|
+
emit_progress,
|
|
36
|
+
)
|
|
29
37
|
from .result import RESULT_FORMAT_VERSION, materialize_result_database
|
|
30
38
|
from .runner import ToolCancelledError, ToolCommand, ToolRunResult, ToolRunner
|
|
31
39
|
from .sequence import (
|
|
@@ -134,6 +142,7 @@ def run_annotation(
|
|
|
134
142
|
threads: int = 1,
|
|
135
143
|
output_format: Literal["duckdb"] = "duckdb",
|
|
136
144
|
result_metadata: Mapping[str, str] | None = None,
|
|
145
|
+
progress_sink: ProgressSink | None = None,
|
|
137
146
|
) -> AnnotationSummary:
|
|
138
147
|
"""Resolve exact evidence, compute misses, and publish one DuckDB result."""
|
|
139
148
|
|
|
@@ -148,11 +157,30 @@ def run_annotation(
|
|
|
148
157
|
|
|
149
158
|
started = time.perf_counter()
|
|
150
159
|
fasta_started = time.perf_counter()
|
|
160
|
+
emit_progress(
|
|
161
|
+
progress_sink,
|
|
162
|
+
ProgressEvent(
|
|
163
|
+
ProgressPhase.STAGING,
|
|
164
|
+
ProgressState.STARTED,
|
|
165
|
+
"staging FASTA",
|
|
166
|
+
),
|
|
167
|
+
)
|
|
151
168
|
fasta_stage_root = Path(
|
|
152
169
|
tempfile.mkdtemp(prefix=".seqevi-fasta-", dir=output_dir.parent)
|
|
153
170
|
)
|
|
154
171
|
stage = stage_fasta(fasta_path, fasta_stage_root)
|
|
155
172
|
fasta_staging_seconds = time.perf_counter() - fasta_started
|
|
173
|
+
emit_progress(
|
|
174
|
+
progress_sink,
|
|
175
|
+
ProgressEvent(
|
|
176
|
+
ProgressPhase.STAGING,
|
|
177
|
+
ProgressState.COMPLETED,
|
|
178
|
+
"FASTA staged",
|
|
179
|
+
completed=stage.unique_sequences,
|
|
180
|
+
total=stage.unique_sequences,
|
|
181
|
+
unit=ProgressUnit.SEQUENCES,
|
|
182
|
+
),
|
|
183
|
+
)
|
|
156
184
|
try:
|
|
157
185
|
work_dir = Path(
|
|
158
186
|
tempfile.mkdtemp(prefix=".seqevi-annotate-", dir=output_dir.parent)
|
|
@@ -201,6 +229,14 @@ def run_annotation(
|
|
|
201
229
|
claim_session.cancellation_signal if claim_session is not None else None
|
|
202
230
|
),
|
|
203
231
|
)
|
|
232
|
+
emit_progress(
|
|
233
|
+
progress_sink,
|
|
234
|
+
ProgressEvent(
|
|
235
|
+
ProgressPhase.STORE_LOOKUP,
|
|
236
|
+
ProgressState.STARTED,
|
|
237
|
+
"store lookup",
|
|
238
|
+
),
|
|
239
|
+
)
|
|
204
240
|
|
|
205
241
|
for identity_batch in batched(iter_staged_identities(stage), _STORE_BATCH_SIZE):
|
|
206
242
|
queries = tuple(
|
|
@@ -238,6 +274,17 @@ def run_annotation(
|
|
|
238
274
|
cached = store.lookup_many(queries)
|
|
239
275
|
store_lookup_seconds += time.perf_counter() - lookup_started
|
|
240
276
|
lookup_batches += 1
|
|
277
|
+
emit_progress(
|
|
278
|
+
progress_sink,
|
|
279
|
+
ProgressEvent(
|
|
280
|
+
ProgressPhase.STORE_LOOKUP,
|
|
281
|
+
ProgressState.RUNNING,
|
|
282
|
+
"store lookup",
|
|
283
|
+
completed=len(keys_by_sequence_id),
|
|
284
|
+
total=stage.unique_sequences,
|
|
285
|
+
unit=ProgressUnit.SEQUENCES,
|
|
286
|
+
),
|
|
287
|
+
)
|
|
241
288
|
if claim_session is None:
|
|
242
289
|
for query in queries:
|
|
243
290
|
if query.key not in cached:
|
|
@@ -259,6 +306,7 @@ def run_annotation(
|
|
|
259
306
|
timeout_seconds=timeout_seconds,
|
|
260
307
|
threads=threads,
|
|
261
308
|
claim_session=claim_session,
|
|
309
|
+
progress_sink=progress_sink,
|
|
262
310
|
)
|
|
263
311
|
commit_batches += batch_metrics.commit_batches
|
|
264
312
|
adapter_seconds += batch_metrics.adapter_seconds
|
|
@@ -281,6 +329,7 @@ def run_annotation(
|
|
|
281
329
|
timeout_seconds=timeout_seconds,
|
|
282
330
|
threads=threads,
|
|
283
331
|
claim_session=claim_session,
|
|
332
|
+
progress_sink=progress_sink,
|
|
284
333
|
)
|
|
285
334
|
commit_batches += batch_metrics.commit_batches
|
|
286
335
|
adapter_seconds += batch_metrics.adapter_seconds
|
|
@@ -288,7 +337,16 @@ def run_annotation(
|
|
|
288
337
|
existing_finalizations += batch_metrics.existing_finalizations
|
|
289
338
|
computed_ids.difference_update(batch_metrics.peer_completed_sequence_ids)
|
|
290
339
|
|
|
340
|
+
waited_for_claim = bool(busy_queries)
|
|
291
341
|
while busy_queries:
|
|
342
|
+
emit_progress(
|
|
343
|
+
progress_sink,
|
|
344
|
+
ProgressEvent(
|
|
345
|
+
ProgressPhase.CLAIM_WAIT,
|
|
346
|
+
ProgressState.RUNNING,
|
|
347
|
+
"waiting for peer claims",
|
|
348
|
+
),
|
|
349
|
+
)
|
|
292
350
|
time.sleep(busy_retry_after or 1.0)
|
|
293
351
|
next_retry_after: float | None = None
|
|
294
352
|
next_busy = []
|
|
@@ -328,6 +386,7 @@ def run_annotation(
|
|
|
328
386
|
timeout_seconds=timeout_seconds,
|
|
329
387
|
threads=threads,
|
|
330
388
|
claim_session=claim_session,
|
|
389
|
+
progress_sink=progress_sink,
|
|
331
390
|
)
|
|
332
391
|
commit_batches += batch_metrics.commit_batches
|
|
333
392
|
adapter_seconds += batch_metrics.adapter_seconds
|
|
@@ -339,8 +398,38 @@ def run_annotation(
|
|
|
339
398
|
busy_queries = next_busy
|
|
340
399
|
busy_retry_after = next_retry_after
|
|
341
400
|
|
|
401
|
+
if waited_for_claim:
|
|
402
|
+
emit_progress(
|
|
403
|
+
progress_sink,
|
|
404
|
+
ProgressEvent(
|
|
405
|
+
ProgressPhase.CLAIM_WAIT,
|
|
406
|
+
ProgressState.COMPLETED,
|
|
407
|
+
"peer claims resolved",
|
|
408
|
+
),
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
emit_progress(
|
|
412
|
+
progress_sink,
|
|
413
|
+
ProgressEvent(
|
|
414
|
+
ProgressPhase.STORE_LOOKUP,
|
|
415
|
+
ProgressState.COMPLETED,
|
|
416
|
+
"store lookup completed",
|
|
417
|
+
completed=stage.unique_sequences,
|
|
418
|
+
total=stage.unique_sequences,
|
|
419
|
+
unit=ProgressUnit.SEQUENCES,
|
|
420
|
+
),
|
|
421
|
+
)
|
|
422
|
+
|
|
342
423
|
if claim_session is not None:
|
|
343
424
|
claim_session.raise_if_lost()
|
|
425
|
+
emit_progress(
|
|
426
|
+
progress_sink,
|
|
427
|
+
ProgressEvent(
|
|
428
|
+
ProgressPhase.STORE_FETCH,
|
|
429
|
+
ProgressState.STARTED,
|
|
430
|
+
"fetching evidence",
|
|
431
|
+
),
|
|
432
|
+
)
|
|
344
433
|
fetch_started = time.perf_counter()
|
|
345
434
|
fetched_by_key = store.fetch_many(keys_by_sequence_id.values())
|
|
346
435
|
store_fetch_seconds = time.perf_counter() - fetch_started
|
|
@@ -351,6 +440,17 @@ def run_annotation(
|
|
|
351
440
|
raise AnnotationError(
|
|
352
441
|
f"Store did not expose {len(missing_keys)} terminal evidence records"
|
|
353
442
|
)
|
|
443
|
+
emit_progress(
|
|
444
|
+
progress_sink,
|
|
445
|
+
ProgressEvent(
|
|
446
|
+
ProgressPhase.STORE_FETCH,
|
|
447
|
+
ProgressState.COMPLETED,
|
|
448
|
+
"evidence fetched",
|
|
449
|
+
completed=len(fetched_by_key),
|
|
450
|
+
total=stage.unique_sequences,
|
|
451
|
+
unit=ProgressUnit.SEQUENCES,
|
|
452
|
+
),
|
|
453
|
+
)
|
|
354
454
|
fetched_by_sequence_id = {
|
|
355
455
|
key.sequence_id: fetched for key, fetched in fetched_by_key.items()
|
|
356
456
|
}
|
|
@@ -366,6 +466,14 @@ def run_annotation(
|
|
|
366
466
|
statuses = [
|
|
367
467
|
fetched.record.status for fetched in fetched_by_sequence_id.values()
|
|
368
468
|
]
|
|
469
|
+
emit_progress(
|
|
470
|
+
progress_sink,
|
|
471
|
+
ProgressEvent(
|
|
472
|
+
ProgressPhase.PACKAGE,
|
|
473
|
+
ProgressState.STARTED,
|
|
474
|
+
"packaging result",
|
|
475
|
+
),
|
|
476
|
+
)
|
|
369
477
|
package_started = time.perf_counter()
|
|
370
478
|
if output_format != "duckdb":
|
|
371
479
|
raise AnnotationError(f"unsupported output format: {output_format}")
|
|
@@ -413,6 +521,17 @@ def run_annotation(
|
|
|
413
521
|
unlink_owned_output()
|
|
414
522
|
raise
|
|
415
523
|
package_seconds = time.perf_counter() - package_started
|
|
524
|
+
emit_progress(
|
|
525
|
+
progress_sink,
|
|
526
|
+
ProgressEvent(
|
|
527
|
+
ProgressPhase.PACKAGE,
|
|
528
|
+
ProgressState.COMPLETED,
|
|
529
|
+
"result packaged",
|
|
530
|
+
completed=stage.unique_sequences,
|
|
531
|
+
total=stage.unique_sequences,
|
|
532
|
+
unit=ProgressUnit.SEQUENCES,
|
|
533
|
+
),
|
|
534
|
+
)
|
|
416
535
|
except BaseException as error:
|
|
417
536
|
primary_failure = error
|
|
418
537
|
if not isinstance(error, Exception):
|
|
@@ -538,6 +657,7 @@ def _run_annotation_batch(
|
|
|
538
657
|
timeout_seconds: float | None,
|
|
539
658
|
threads: int,
|
|
540
659
|
claim_session: ClaimSession | None = None,
|
|
660
|
+
progress_sink: ProgressSink | None = None,
|
|
541
661
|
) -> _BatchMetrics:
|
|
542
662
|
batch_dir = work_dir / f"batch-{batch_number:06d}"
|
|
543
663
|
batch_dir.mkdir()
|
|
@@ -546,6 +666,15 @@ def _run_annotation_batch(
|
|
|
546
666
|
claim_session.raise_if_lost()
|
|
547
667
|
with misses_fasta.open("w", encoding="ascii", newline="\n") as handle:
|
|
548
668
|
handle.writelines(iter_fasta_lines(identities))
|
|
669
|
+
tool_message = f"{_tool_display_name(adapter)} · tool batch {batch_number}"
|
|
670
|
+
emit_progress(
|
|
671
|
+
progress_sink,
|
|
672
|
+
ProgressEvent(
|
|
673
|
+
ProgressPhase.TOOL,
|
|
674
|
+
ProgressState.STARTED,
|
|
675
|
+
f"{tool_message} running",
|
|
676
|
+
),
|
|
677
|
+
)
|
|
549
678
|
adapter_started = time.perf_counter()
|
|
550
679
|
try:
|
|
551
680
|
batch = adapter.run_batch(
|
|
@@ -561,12 +690,31 @@ def _run_annotation_batch(
|
|
|
561
690
|
claim_session.raise_if_lost()
|
|
562
691
|
raise
|
|
563
692
|
adapter_seconds = time.perf_counter() - adapter_started
|
|
693
|
+
emit_progress(
|
|
694
|
+
progress_sink,
|
|
695
|
+
ProgressEvent(
|
|
696
|
+
ProgressPhase.TOOL,
|
|
697
|
+
ProgressState.COMPLETED,
|
|
698
|
+
f"{tool_message} completed",
|
|
699
|
+
completed=len(identities),
|
|
700
|
+
total=len(identities),
|
|
701
|
+
unit=ProgressUnit.SEQUENCES,
|
|
702
|
+
),
|
|
703
|
+
)
|
|
564
704
|
if claim_session is not None:
|
|
565
705
|
claim_session.raise_if_lost()
|
|
566
706
|
commits = _build_commits(batch=batch, identities=identities, adapter=adapter)
|
|
567
707
|
batches = 0
|
|
568
708
|
peer_completed_sequence_ids: set[str] = set()
|
|
569
709
|
existing_finalizations = 0
|
|
710
|
+
emit_progress(
|
|
711
|
+
progress_sink,
|
|
712
|
+
ProgressEvent(
|
|
713
|
+
ProgressPhase.STORE_COMMIT,
|
|
714
|
+
ProgressState.STARTED,
|
|
715
|
+
f"committing tool batch {batch_number}",
|
|
716
|
+
),
|
|
717
|
+
)
|
|
570
718
|
commit_started = time.perf_counter()
|
|
571
719
|
for commit_batch in batched(commits, _STORE_BATCH_SIZE):
|
|
572
720
|
if claim_session is None:
|
|
@@ -585,6 +733,17 @@ def _run_annotation_batch(
|
|
|
585
733
|
outcome is CommitOutcome.EXISTING for outcome in outcomes
|
|
586
734
|
)
|
|
587
735
|
batches += 1
|
|
736
|
+
emit_progress(
|
|
737
|
+
progress_sink,
|
|
738
|
+
ProgressEvent(
|
|
739
|
+
ProgressPhase.STORE_COMMIT,
|
|
740
|
+
ProgressState.COMPLETED,
|
|
741
|
+
f"tool batch {batch_number} committed",
|
|
742
|
+
completed=len(identities),
|
|
743
|
+
total=len(identities),
|
|
744
|
+
unit=ProgressUnit.SEQUENCES,
|
|
745
|
+
),
|
|
746
|
+
)
|
|
588
747
|
return _BatchMetrics(
|
|
589
748
|
commit_batches=batches,
|
|
590
749
|
adapter_seconds=adapter_seconds,
|
|
@@ -594,6 +753,14 @@ def _run_annotation_batch(
|
|
|
594
753
|
)
|
|
595
754
|
|
|
596
755
|
|
|
756
|
+
def _tool_display_name(adapter: AnnotationAdapter) -> str:
|
|
757
|
+
return {
|
|
758
|
+
"eggnog": "eggNOG-mapper",
|
|
759
|
+
"interpro-pfam": "InterProScan/Pfam",
|
|
760
|
+
"dbcan-cazyme": "dbCAN",
|
|
761
|
+
}.get(adapter.contract.name, adapter.contract.name)
|
|
762
|
+
|
|
763
|
+
|
|
597
764
|
def _build_commits(
|
|
598
765
|
*,
|
|
599
766
|
batch: AdapterBatchResult,
|
|
@@ -24,6 +24,13 @@ from .execution_profile import (
|
|
|
24
24
|
load_execution_profile,
|
|
25
25
|
load_named_profile,
|
|
26
26
|
)
|
|
27
|
+
from .progress import (
|
|
28
|
+
ProgressEvent,
|
|
29
|
+
ProgressPhase,
|
|
30
|
+
ProgressSink,
|
|
31
|
+
ProgressState,
|
|
32
|
+
emit_progress,
|
|
33
|
+
)
|
|
27
34
|
from .result import RESULT_FORMAT_VERSION, scan_annotations as _scan_annotations
|
|
28
35
|
from .store import open_evidence_store
|
|
29
36
|
|
|
@@ -162,7 +169,16 @@ def _run_annotation_application(
|
|
|
162
169
|
threads: int | None,
|
|
163
170
|
timeout_seconds: float | None,
|
|
164
171
|
adapter_factory: Callable[[AdapterConfiguration], AnnotationAdapter] | None = None,
|
|
172
|
+
progress_sink: ProgressSink | None = None,
|
|
165
173
|
) -> AnnotationInvocation:
|
|
174
|
+
emit_progress(
|
|
175
|
+
progress_sink,
|
|
176
|
+
ProgressEvent(
|
|
177
|
+
ProgressPhase.ANNOTATION,
|
|
178
|
+
ProgressState.STARTED,
|
|
179
|
+
"starting annotation",
|
|
180
|
+
),
|
|
181
|
+
)
|
|
166
182
|
inputs = resolve_annotation_inputs(
|
|
167
183
|
profile=profile,
|
|
168
184
|
config=config,
|
|
@@ -174,6 +190,14 @@ def _run_annotation_application(
|
|
|
174
190
|
timeout_seconds=timeout_seconds,
|
|
175
191
|
)
|
|
176
192
|
if inputs.profile is not None and inputs.profile.version == 2:
|
|
193
|
+
emit_progress(
|
|
194
|
+
progress_sink,
|
|
195
|
+
ProgressEvent(
|
|
196
|
+
ProgressPhase.ANNOTATION,
|
|
197
|
+
ProgressState.RUNNING,
|
|
198
|
+
"managed annotation running",
|
|
199
|
+
),
|
|
200
|
+
)
|
|
177
201
|
managed = run_oci_annotation(
|
|
178
202
|
fasta=fasta.expanduser().resolve(),
|
|
179
203
|
output=output.expanduser().resolve(),
|
|
@@ -182,12 +206,29 @@ def _run_annotation_application(
|
|
|
182
206
|
threads=inputs.threads,
|
|
183
207
|
timeout_seconds=inputs.timeout_seconds,
|
|
184
208
|
)
|
|
185
|
-
|
|
209
|
+
emit_progress(
|
|
210
|
+
progress_sink,
|
|
211
|
+
ProgressEvent(
|
|
212
|
+
ProgressPhase.FINALIZATION,
|
|
213
|
+
ProgressState.STARTED,
|
|
214
|
+
"validating result",
|
|
215
|
+
),
|
|
216
|
+
)
|
|
217
|
+
invocation = AnnotationInvocation(
|
|
186
218
|
relation=_scan_annotations(managed.output),
|
|
187
219
|
summary=managed.summary,
|
|
188
220
|
adapter=managed.adapter,
|
|
189
221
|
result_schema_id=managed.result_schema_id,
|
|
190
222
|
)
|
|
223
|
+
emit_progress(
|
|
224
|
+
progress_sink,
|
|
225
|
+
ProgressEvent(
|
|
226
|
+
ProgressPhase.COMPLETED,
|
|
227
|
+
ProgressState.COMPLETED,
|
|
228
|
+
"annotation completed",
|
|
229
|
+
),
|
|
230
|
+
)
|
|
231
|
+
return invocation
|
|
191
232
|
if inputs.executable is None:
|
|
192
233
|
raise AnnotationError("local annotation requires an executable")
|
|
193
234
|
factory = create_adapter if adapter_factory is None else adapter_factory
|
|
@@ -210,13 +251,31 @@ def _run_annotation_application(
|
|
|
210
251
|
threads=inputs.threads,
|
|
211
252
|
output_format="duckdb",
|
|
212
253
|
result_metadata=metadata,
|
|
254
|
+
progress_sink=progress_sink,
|
|
255
|
+
)
|
|
256
|
+
emit_progress(
|
|
257
|
+
progress_sink,
|
|
258
|
+
ProgressEvent(
|
|
259
|
+
ProgressPhase.FINALIZATION,
|
|
260
|
+
ProgressState.STARTED,
|
|
261
|
+
"closing Store and validating result",
|
|
262
|
+
),
|
|
213
263
|
)
|
|
214
|
-
|
|
264
|
+
invocation = AnnotationInvocation(
|
|
215
265
|
relation=_scan_annotations(summary.output_dir),
|
|
216
266
|
summary=summary,
|
|
217
267
|
adapter=metadata["Adapter"],
|
|
218
268
|
result_schema_id=metadata["ResultSchemaID"],
|
|
219
269
|
)
|
|
270
|
+
emit_progress(
|
|
271
|
+
progress_sink,
|
|
272
|
+
ProgressEvent(
|
|
273
|
+
ProgressPhase.COMPLETED,
|
|
274
|
+
ProgressState.COMPLETED,
|
|
275
|
+
"annotation completed",
|
|
276
|
+
),
|
|
277
|
+
)
|
|
278
|
+
return invocation
|
|
220
279
|
|
|
221
280
|
|
|
222
281
|
def resolve_annotation_inputs(
|
|
@@ -3,10 +3,14 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
import logging
|
|
6
7
|
import shutil
|
|
7
8
|
import sys
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
from collections.abc import Callable
|
|
8
12
|
from pathlib import Path
|
|
9
|
-
from typing import Annotated, NoReturn
|
|
13
|
+
from typing import Annotated, NoReturn, TextIO
|
|
10
14
|
|
|
11
15
|
import typer
|
|
12
16
|
|
|
@@ -27,8 +31,12 @@ from .execution_profile import (
|
|
|
27
31
|
profile_example,
|
|
28
32
|
redacted_effective_configuration,
|
|
29
33
|
)
|
|
34
|
+
from .progress import ProgressEvent, ProgressState
|
|
30
35
|
from .resource_lock import resource_lock_path
|
|
31
36
|
|
|
37
|
+
_LOGGER = logging.getLogger(__name__)
|
|
38
|
+
_PROGRESS_REFRESH_SECONDS = 1.0
|
|
39
|
+
|
|
32
40
|
app = typer.Typer(
|
|
33
41
|
name="seqevi",
|
|
34
42
|
help="Content-addressed protein sequence annotation evidence.",
|
|
@@ -63,6 +71,116 @@ def _resolve_executable(value: str) -> Path:
|
|
|
63
71
|
return Path(resolved).resolve()
|
|
64
72
|
|
|
65
73
|
|
|
74
|
+
class _ProgressRenderer:
|
|
75
|
+
"""Render the latest private progress event on one transient stderr line."""
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
*,
|
|
80
|
+
stream: TextIO,
|
|
81
|
+
refresh_seconds: float = _PROGRESS_REFRESH_SECONDS,
|
|
82
|
+
monotonic: Callable[[], float] = time.monotonic,
|
|
83
|
+
) -> None:
|
|
84
|
+
self._stream = stream
|
|
85
|
+
self._refresh_seconds = refresh_seconds
|
|
86
|
+
self._monotonic = monotonic
|
|
87
|
+
self._lock = threading.Lock()
|
|
88
|
+
self._stop = threading.Event()
|
|
89
|
+
self._latest: ProgressEvent | None = None
|
|
90
|
+
self._phase_started = monotonic()
|
|
91
|
+
self._line_visible = False
|
|
92
|
+
self._thread: threading.Thread | None = None
|
|
93
|
+
|
|
94
|
+
def __call__(self, event: ProgressEvent) -> None:
|
|
95
|
+
now = self._monotonic()
|
|
96
|
+
with self._lock:
|
|
97
|
+
if (
|
|
98
|
+
self._latest is None
|
|
99
|
+
or self._latest.phase is not event.phase
|
|
100
|
+
or event.state is ProgressState.STARTED
|
|
101
|
+
):
|
|
102
|
+
self._phase_started = now
|
|
103
|
+
self._latest = event
|
|
104
|
+
self._write_locked(now)
|
|
105
|
+
if self._thread is None:
|
|
106
|
+
self._thread = threading.Thread(
|
|
107
|
+
target=self._refresh,
|
|
108
|
+
name="seqevi-progress",
|
|
109
|
+
daemon=True,
|
|
110
|
+
)
|
|
111
|
+
self._thread.start()
|
|
112
|
+
|
|
113
|
+
def close(self) -> None:
|
|
114
|
+
"""Stop the timer and remove the transient line before final output."""
|
|
115
|
+
|
|
116
|
+
self._stop.set()
|
|
117
|
+
if self._thread is not None:
|
|
118
|
+
self._thread.join()
|
|
119
|
+
with self._lock:
|
|
120
|
+
if self._line_visible:
|
|
121
|
+
self._stream.write("\r\x1b[K")
|
|
122
|
+
self._stream.flush()
|
|
123
|
+
self._line_visible = False
|
|
124
|
+
|
|
125
|
+
def _refresh(self) -> None:
|
|
126
|
+
try:
|
|
127
|
+
while not self._stop.wait(self._refresh_seconds):
|
|
128
|
+
with self._lock:
|
|
129
|
+
self._write_locked(self._monotonic())
|
|
130
|
+
except Exception:
|
|
131
|
+
_LOGGER.exception("annotation progress renderer refresh failed")
|
|
132
|
+
|
|
133
|
+
def _write_locked(self, now: float) -> None:
|
|
134
|
+
if self._latest is None:
|
|
135
|
+
return
|
|
136
|
+
elapsed = (
|
|
137
|
+
self._latest.elapsed_seconds
|
|
138
|
+
if self._latest.elapsed_seconds is not None
|
|
139
|
+
else max(0.0, now - self._phase_started)
|
|
140
|
+
)
|
|
141
|
+
text = _render_progress_event(self._latest, elapsed_seconds=elapsed)
|
|
142
|
+
self._stream.write(f"\r{text}\x1b[K")
|
|
143
|
+
self._stream.flush()
|
|
144
|
+
self._line_visible = True
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _render_progress_event(event: ProgressEvent, *, elapsed_seconds: float) -> str:
|
|
148
|
+
parts = [f"[seqevi] {event.message}"]
|
|
149
|
+
if event.completed is not None:
|
|
150
|
+
assert event.total is not None
|
|
151
|
+
assert event.unit is not None
|
|
152
|
+
fraction = f"{event.completed:,}/{event.total:,} {event.unit.value}"
|
|
153
|
+
percentage = 100.0 if event.total == 0 else event.completed / event.total * 100
|
|
154
|
+
parts.append(f"{fraction} ({percentage:.0f}%)")
|
|
155
|
+
if event.state is not ProgressState.COMPLETED:
|
|
156
|
+
parts.append(_format_elapsed(elapsed_seconds))
|
|
157
|
+
return " · ".join(parts)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _format_elapsed(elapsed_seconds: float) -> str:
|
|
161
|
+
seconds = int(max(0.0, elapsed_seconds))
|
|
162
|
+
hours, remainder = divmod(seconds, 3_600)
|
|
163
|
+
minutes, seconds = divmod(remainder, 60)
|
|
164
|
+
if hours:
|
|
165
|
+
return f"{hours}h{minutes:02d}m{seconds:02d}s"
|
|
166
|
+
if minutes:
|
|
167
|
+
return f"{minutes}m{seconds:02d}s"
|
|
168
|
+
return f"{seconds}s"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _stderr_is_interactive() -> bool:
|
|
172
|
+
return sys.stderr.isatty()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _close_progress_renderer(renderer: _ProgressRenderer | None) -> None:
|
|
176
|
+
if renderer is None:
|
|
177
|
+
return
|
|
178
|
+
try:
|
|
179
|
+
renderer.close()
|
|
180
|
+
except Exception:
|
|
181
|
+
_LOGGER.exception("annotation progress renderer shutdown failed")
|
|
182
|
+
|
|
183
|
+
|
|
66
184
|
@app.callback()
|
|
67
185
|
def root(
|
|
68
186
|
context: typer.Context,
|
|
@@ -177,6 +295,13 @@ def annotate_command(
|
|
|
177
295
|
help="Worker threads; overrides the profile default.",
|
|
178
296
|
),
|
|
179
297
|
] = None,
|
|
298
|
+
progress: Annotated[
|
|
299
|
+
bool,
|
|
300
|
+
typer.Option(
|
|
301
|
+
"--progress",
|
|
302
|
+
help="Show interactive SeqEvi phase and liveness progress on stderr.",
|
|
303
|
+
),
|
|
304
|
+
] = False,
|
|
180
305
|
json_output: Annotated[
|
|
181
306
|
bool,
|
|
182
307
|
typer.Option(
|
|
@@ -187,6 +312,11 @@ def annotate_command(
|
|
|
187
312
|
) -> None:
|
|
188
313
|
"""Reuse exact evidence and publish one immutable DuckDB result."""
|
|
189
314
|
|
|
315
|
+
renderer = (
|
|
316
|
+
_ProgressRenderer(stream=sys.stderr)
|
|
317
|
+
if progress and not json_output and _stderr_is_interactive()
|
|
318
|
+
else None
|
|
319
|
+
)
|
|
190
320
|
try:
|
|
191
321
|
invocation = _run_annotation_application(
|
|
192
322
|
fasta=fasta,
|
|
@@ -200,8 +330,11 @@ def annotate_command(
|
|
|
200
330
|
threads=threads,
|
|
201
331
|
timeout_seconds=timeout_seconds,
|
|
202
332
|
adapter_factory=create_adapter,
|
|
333
|
+
progress_sink=renderer,
|
|
203
334
|
)
|
|
204
335
|
except (AnnotationError, FastaValidationError, StoreError) as error:
|
|
336
|
+
_close_progress_renderer(renderer)
|
|
337
|
+
renderer = None
|
|
205
338
|
if json_output:
|
|
206
339
|
typer.echo(
|
|
207
340
|
json.dumps(
|
|
@@ -218,6 +351,8 @@ def annotate_command(
|
|
|
218
351
|
else:
|
|
219
352
|
typer.echo(f"Error: {error}", err=True)
|
|
220
353
|
raise typer.Exit(code=1) from error
|
|
354
|
+
finally:
|
|
355
|
+
_close_progress_renderer(renderer)
|
|
221
356
|
|
|
222
357
|
summary = invocation.summary
|
|
223
358
|
if json_output:
|