seqevi 0.3.0__tar.gz → 0.3.2__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.
Files changed (80) hide show
  1. {seqevi-0.3.0 → seqevi-0.3.2}/PKG-INFO +3 -3
  2. {seqevi-0.3.0 → seqevi-0.3.2}/README.md +2 -2
  3. {seqevi-0.3.0 → seqevi-0.3.2}/pyproject.toml +2 -1
  4. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/__init__.py +1 -1
  5. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/adapters/interpro_pfam.py +173 -28
  6. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/distribution/manifest.py +9 -0
  7. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/distribution/oci.py +1 -2
  8. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/kits/dbcan-cazyme.toml +2 -1
  9. seqevi-0.3.2/tests/test_acceptance_containment.py +362 -0
  10. {seqevi-0.3.0 → seqevi-0.3.2}/tests/test_acceptance_harnesses.py +96 -13
  11. {seqevi-0.3.0 → seqevi-0.3.2}/tests/test_cli.py +1 -1
  12. {seqevi-0.3.0 → seqevi-0.3.2}/tests/test_dbcan_image_assets.py +18 -0
  13. seqevi-0.3.2/tests/test_dbcan_local_candidate_harness.py +158 -0
  14. {seqevi-0.3.0 → seqevi-0.3.2}/tests/test_interpro_pfam.py +332 -7
  15. {seqevi-0.3.0 → seqevi-0.3.2}/tests/test_managed_setup.py +7 -1
  16. {seqevi-0.3.0 → seqevi-0.3.2}/tests/test_oci.py +66 -3
  17. {seqevi-0.3.0 → seqevi-0.3.2}/LICENSE +0 -0
  18. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/__main__.py +0 -0
  19. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/adapters/__init__.py +0 -0
  20. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/adapters/base.py +0 -0
  21. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/adapters/dbcan_cazyme.py +0 -0
  22. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/adapters/eggnog.py +0 -0
  23. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/adapters/registry.py +0 -0
  24. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/annotate.py +0 -0
  25. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/api.py +0 -0
  26. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/cli.py +0 -0
  27. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/distribution/__init__.py +0 -0
  28. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/distribution/setup.py +0 -0
  29. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/errors.py +0 -0
  30. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/evidence.py +0 -0
  31. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/execution_profile.py +0 -0
  32. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/hashing.py +0 -0
  33. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/kits/__init__.py +0 -0
  34. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/resource_lock.py +0 -0
  35. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/result.py +0 -0
  36. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/runner.py +0 -0
  37. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/runtime_identity.py +0 -0
  38. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/sequence.py +0 -0
  39. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/service/__init__.py +0 -0
  40. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/service/app.py +0 -0
  41. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/service/config.py +0 -0
  42. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/service/persistence.py +0 -0
  43. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/store/__init__.py +0 -0
  44. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/store/_artifact_reader.py +0 -0
  45. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/store/_resolver.py +0 -0
  46. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/store/artifact.py +0 -0
  47. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/store/client.py +0 -0
  48. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/store/contract.py +0 -0
  49. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/store/factory.py +0 -0
  50. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/store/local.py +0 -0
  51. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/store/migration.py +0 -0
  52. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/store/migrations/__init__.py +0 -0
  53. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/store/migrations/env.py +0 -0
  54. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/store/migrations/versions/0001_initial_store.py +0 -0
  55. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/store/migrations/versions/0002_artifact_byte_size_bigint.py +0 -0
  56. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/store/migrations/versions/0003_evidence_claim_leases.py +0 -0
  57. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/store/migrations/versions/0004_claim_sessions.py +0 -0
  58. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/store/migrations/versions/__init__.py +0 -0
  59. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/store/schema.py +0 -0
  60. {seqevi-0.3.0 → seqevi-0.3.2}/src/seqevi/store/transport.py +0 -0
  61. {seqevi-0.3.0 → seqevi-0.3.2}/tests/__init__.py +0 -0
  62. {seqevi-0.3.0 → seqevi-0.3.2}/tests/support.py +0 -0
  63. {seqevi-0.3.0 → seqevi-0.3.2}/tests/test_annotate.py +0 -0
  64. {seqevi-0.3.0 → seqevi-0.3.2}/tests/test_api.py +0 -0
  65. {seqevi-0.3.0 → seqevi-0.3.2}/tests/test_architecture_contracts.py +0 -0
  66. {seqevi-0.3.0 → seqevi-0.3.2}/tests/test_artifact_store.py +0 -0
  67. {seqevi-0.3.0 → seqevi-0.3.2}/tests/test_claim_leases.py +0 -0
  68. {seqevi-0.3.0 → seqevi-0.3.2}/tests/test_dbcan_cazyme.py +0 -0
  69. {seqevi-0.3.0 → seqevi-0.3.2}/tests/test_deployment_assets.py +0 -0
  70. {seqevi-0.3.0 → seqevi-0.3.2}/tests/test_eggnog.py +0 -0
  71. {seqevi-0.3.0 → seqevi-0.3.2}/tests/test_evidence.py +0 -0
  72. {seqevi-0.3.0 → seqevi-0.3.2}/tests/test_execution_profile.py +0 -0
  73. {seqevi-0.3.0 → seqevi-0.3.2}/tests/test_incremental_reuse.py +0 -0
  74. {seqevi-0.3.0 → seqevi-0.3.2}/tests/test_local_store.py +0 -0
  75. {seqevi-0.3.0 → seqevi-0.3.2}/tests/test_resource_lock.py +0 -0
  76. {seqevi-0.3.0 → seqevi-0.3.2}/tests/test_result_prototype.py +0 -0
  77. {seqevi-0.3.0 → seqevi-0.3.2}/tests/test_runner.py +0 -0
  78. {seqevi-0.3.0 → seqevi-0.3.2}/tests/test_runtime_identity.py +0 -0
  79. {seqevi-0.3.0 → seqevi-0.3.2}/tests/test_sequence.py +0 -0
  80. {seqevi-0.3.0 → seqevi-0.3.2}/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.0
3
+ Version: 0.3.2
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
- 0.3.0 source tree uses strict protein sequence identity, the single-host
48
+ The 0.3.2 candidate 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.0 provides managed setup for dbCAN only. The `eggnog` and
52
+ SeqEvi 0.3.2 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
- 0.3.0 source tree uses strict protein sequence identity, the single-host
14
+ The 0.3.2 candidate 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.0 provides managed setup for dbCAN only. The `eggnog` and
18
+ SeqEvi 0.3.2 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)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "seqevi"
3
- version = "0.3.0"
3
+ version = "0.3.2"
4
4
  description = "Content-addressed, reusable protein sequence annotation evidence."
5
5
  authors = [
6
6
  { name = "Fuqing Zhang", email = "fu.qing.zhang.work@gmail.com" },
@@ -115,6 +115,7 @@ testpaths = [
115
115
  ]
116
116
  pythonpath = [
117
117
  "src",
118
+ ".",
118
119
  ]
119
120
  addopts = [
120
121
  "-ra",
@@ -1,6 +1,6 @@
1
1
  """Content-addressed protein sequence annotation evidence."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.3.2"
4
4
 
5
5
  from .api import annotate, scan_annotations
6
6
 
@@ -28,7 +28,7 @@ from seqevi.sequence import SequenceIdentity
28
28
 
29
29
  from .base import AdapterBatchResult, AdapterContract, AdapterSequenceResult
30
30
 
31
- ADAPTER_CONTRACT_VERSION = "interpro-pfam/1"
31
+ ADAPTER_CONTRACT_VERSION = "interpro-pfam/2"
32
32
 
33
33
  INTERPRO_PFAM_EVIDENCE_SCHEMA: Mapping[str, pl.DataType] = {
34
34
  "SequenceID": pl.String(),
@@ -53,6 +53,7 @@ _INTERPRO_ACCESSION_PATTERN = re.compile(r"IPR\d{6}\Z")
53
53
  _TSV_COLUMN_COUNT = 15
54
54
  _PROBE_TIMEOUT_SECONDS = 120.0
55
55
  _NORMALIZED_ROW_BATCH_SIZE = 1000
56
+ _JAVA_OPTION_VARIABLES = ("JDK_JAVA_OPTIONS", "JAVA_TOOL_OPTIONS", "_JAVA_OPTIONS")
56
57
 
57
58
 
58
59
  @dataclass(frozen=True, slots=True)
@@ -77,7 +78,7 @@ class InterProPfamParameters:
77
78
  )
78
79
  if values != ("Pfam", "TSV", True, False, False, "protein"):
79
80
  raise ValueError(
80
- "interpro-pfam/1 uses one fixed direct-scan scientific contract"
81
+ "interpro-pfam/2 uses one fixed direct-scan scientific contract"
81
82
  )
82
83
 
83
84
  def as_semantic_parameters(self) -> dict[str, object]:
@@ -102,6 +103,29 @@ class InterProPfamAdapter:
102
103
  self.database = database.resolve()
103
104
  self.parameters = parameters or InterProPfamParameters()
104
105
  self.environment = dict(environment or {})
106
+ if "PATH" not in self.environment:
107
+ inherited_path = os.environ.get("PATH")
108
+ if inherited_path is None:
109
+ raise AdapterError(
110
+ "InterProScan runtime has no inherited or profile PATH"
111
+ )
112
+ self.environment["PATH"] = inherited_path
113
+ path_entries = self.environment["PATH"].split(os.pathsep)
114
+ if any(not entry or not Path(entry).is_absolute() for entry in path_entries):
115
+ raise AdapterError(
116
+ "InterProScan runtime PATH entries must be non-empty absolute paths"
117
+ )
118
+ for name in _JAVA_OPTION_VARIABLES:
119
+ effective_value = self.environment.get(name, os.environ.get(name, ""))
120
+ if effective_value:
121
+ raise AdapterError(
122
+ f"InterProScan runtime does not allow non-empty {name}"
123
+ )
124
+ self.environment[name] = ""
125
+ java_path, java_home = _resolve_selected_java(self.environment)
126
+ self.environment["PATH"] = os.pathsep.join(
127
+ (str(java_path.parent), self.environment["PATH"])
128
+ )
105
129
  self.install_dir = self.executable.parent
106
130
  self.properties_path = self.install_dir / "interproscan.properties"
107
131
  self.jar_path = self.install_dir / "interproscan-5.jar"
@@ -122,7 +146,7 @@ class InterProPfamAdapter:
122
146
  jar_path=self.jar_path,
123
147
  properties=self.properties,
124
148
  version=self.interproscan_version,
125
- environment=self.environment,
149
+ java_home=java_home,
126
150
  )
127
151
  resource_id = _calculate_resource_id(
128
152
  database=self.database,
@@ -352,7 +376,7 @@ def _calculate_runtime_digest(
352
376
  jar_path: Path,
353
377
  properties: Mapping[str, str],
354
378
  version: str,
355
- environment: Mapping[str, str],
379
+ java_home: Path,
356
380
  ) -> str:
357
381
  bin_dir = _resolve_install_path(
358
382
  install_dir,
@@ -370,12 +394,6 @@ def _calculate_runtime_digest(
370
394
  if not hmmer_files:
371
395
  raise AdapterError(f"InterProScan HMMER directory is empty: {hmmer_dir}")
372
396
 
373
- java = shutil.which(
374
- "java",
375
- path=environment.get("PATH", os.environ.get("PATH")),
376
- )
377
- if java is None:
378
- raise AdapterError("InterProScan runtime has no Java executable")
379
397
  normalized_properties = {
380
398
  **properties,
381
399
  "data.directory": "${data.directory}",
@@ -388,25 +406,152 @@ def _calculate_runtime_digest(
388
406
  separators=(",", ":"),
389
407
  ).encode("utf-8")
390
408
  )
391
- components = (
392
- RuntimeComponent("launcher", executable),
393
- RuntimeComponent("java", Path(java).resolve()),
394
- RuntimeComponent("interproscan-5.jar", jar_path),
395
- *(
396
- RuntimeComponent(
397
- f"hmmer/{path.relative_to(hmmer_dir).as_posix()}",
398
- path,
409
+ try:
410
+ jdk_components, jdk_snapshot = _jdk_runtime_components(java_home)
411
+ components = (
412
+ RuntimeComponent("launcher", executable),
413
+ RuntimeComponent("interproscan-5.jar", jar_path),
414
+ *jdk_components,
415
+ *(
416
+ RuntimeComponent(
417
+ f"hmmer/{path.relative_to(hmmer_dir).as_posix()}",
418
+ path,
419
+ )
420
+ for path in hmmer_files
421
+ ),
422
+ )
423
+ digest = calculate_runtime_digest(
424
+ runtime_name="interproscan-pfam",
425
+ versions={
426
+ "interproscan": version,
427
+ "properties": f"sha256:{properties_digest}",
428
+ },
429
+ components=components,
430
+ )
431
+ _, after_snapshot = _jdk_runtime_components(java_home)
432
+ except (OSError, UnicodeError) as error:
433
+ raise AdapterError(
434
+ f"cannot read selected JAVA_HOME runtime: {error}"
435
+ ) from error
436
+ if after_snapshot != jdk_snapshot:
437
+ raise AdapterError("selected JAVA_HOME changed while hashing")
438
+ return digest
439
+
440
+
441
+ def _resolve_selected_java(environment: Mapping[str, str]) -> tuple[Path, Path]:
442
+ """Resolve the launcher's frozen PATH selection and its JDK layout."""
443
+
444
+ selected = shutil.which("java", path=environment["PATH"])
445
+ if selected is None:
446
+ raise AdapterError("InterProScan runtime has no Java executable")
447
+ java_path = Path(selected).resolve()
448
+ if java_path.name != "java" or java_path.parent.name != "bin":
449
+ raise AdapterError(
450
+ f"resolved Java executable must be JAVA_HOME/bin/java: {java_path}"
451
+ )
452
+ return java_path, java_path.parent.parent
453
+
454
+
455
+ def _jdk_runtime_components(
456
+ java_home: Path,
457
+ ) -> tuple[tuple[RuntimeComponent, ...], tuple[tuple[object, ...], ...]]:
458
+ release_path = java_home / "release"
459
+ roots = (java_home / "bin", java_home / "conf", java_home / "lib")
460
+ entries: list[tuple[str, Path]] = [("jdk/release", release_path)]
461
+ snapshot: list[tuple[object, ...]] = []
462
+
463
+ def fail(error: OSError) -> None:
464
+ raise error
465
+
466
+ for root in roots:
467
+ if root.is_symlink() or not root.is_dir():
468
+ raise AdapterError(
469
+ "selected JAVA_HOME runtime directory is missing or a symbolic "
470
+ f"link: {root}"
399
471
  )
400
- for path in hmmer_files
401
- ),
472
+ root_file_count = 0
473
+ for directory, directory_names, file_names in os.walk(
474
+ root, followlinks=False, onerror=fail
475
+ ):
476
+ directory_path = Path(directory)
477
+ relative_directory = directory_path.relative_to(java_home).as_posix()
478
+ directory_stat = directory_path.stat(follow_symlinks=False)
479
+ snapshot.append(
480
+ (
481
+ relative_directory,
482
+ "directory",
483
+ directory_stat.st_dev,
484
+ directory_stat.st_ino,
485
+ directory_stat.st_size,
486
+ directory_stat.st_mtime_ns,
487
+ directory_stat.st_ctime_ns,
488
+ )
489
+ )
490
+ for name in sorted((*directory_names, *file_names)):
491
+ path = directory_path / name
492
+ if path.is_symlink():
493
+ raise AdapterError(
494
+ f"selected JAVA_HOME runtime contains a symbolic link: {path}"
495
+ )
496
+ for name in sorted(file_names):
497
+ path = directory_path / name
498
+ relative = path.relative_to(java_home).as_posix()
499
+ file_stat = path.stat(follow_symlinks=False)
500
+ if not path.is_file():
501
+ raise AdapterError(
502
+ "selected JAVA_HOME runtime entry is not a regular file: "
503
+ f"{path}"
504
+ )
505
+ snapshot.append(
506
+ (
507
+ relative,
508
+ "file",
509
+ file_stat.st_dev,
510
+ file_stat.st_ino,
511
+ file_stat.st_size,
512
+ file_stat.st_mtime_ns,
513
+ file_stat.st_ctime_ns,
514
+ )
515
+ )
516
+ entries.append((f"jdk/{relative}", path))
517
+ root_file_count += 1
518
+ if root_file_count == 0:
519
+ raise AdapterError(
520
+ f"selected JAVA_HOME runtime directory has no regular files: {root}"
521
+ )
522
+
523
+ if not release_path.is_file() or release_path.is_symlink():
524
+ raise AdapterError(
525
+ f"selected JAVA_HOME release file is missing or invalid: {release_path}"
526
+ )
527
+ release_stat = release_path.stat(follow_symlinks=False)
528
+ release_text = release_path.read_text(encoding="utf-8")
529
+ version_match = re.search(r'^JAVA_VERSION="?(\d+)', release_text, re.MULTILINE)
530
+ if version_match is None:
531
+ raise AdapterError(
532
+ f"selected JAVA_HOME release file has no JAVA_VERSION: {release_path}"
533
+ )
534
+ if int(version_match.group(1)) >= 11:
535
+ modules_path = java_home / "lib" / "modules"
536
+ if not modules_path.is_file() or modules_path.is_symlink():
537
+ raise AdapterError(
538
+ "selected Java 11+ runtime has no regular lib/modules image: "
539
+ f"{modules_path}"
540
+ )
541
+ snapshot.append(
542
+ (
543
+ "release",
544
+ "file",
545
+ release_stat.st_dev,
546
+ release_stat.st_ino,
547
+ release_stat.st_size,
548
+ release_stat.st_mtime_ns,
549
+ release_stat.st_ctime_ns,
550
+ )
402
551
  )
403
- return calculate_runtime_digest(
404
- runtime_name="interproscan-pfam",
405
- versions={
406
- "interproscan": version,
407
- "properties": f"sha256:{properties_digest}",
408
- },
409
- components=components,
552
+ return (
553
+ tuple(RuntimeComponent(name, path) for name, path in sorted(entries)),
554
+ tuple(sorted(snapshot)),
410
555
  )
411
556
 
412
557
 
@@ -675,7 +820,7 @@ def _parse_tsv_row(
675
820
  if go_annotations != "-" or pathway_annotations != "-":
676
821
  raise AdapterError(
677
822
  f"InterProScan TSV line {line_number} contains GO or pathway annotations "
678
- "outside the interpro-pfam/1 contract"
823
+ "outside the interpro-pfam/2 contract"
679
824
  )
680
825
 
681
826
  return {
@@ -14,6 +14,9 @@ from seqevi.errors import SetupError
14
14
 
15
15
  _SCHEMA_VERSION = 1
16
16
  _KIT_ID = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]*\Z")
17
+ _SEQEVI_VERSION = re.compile(
18
+ r"(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\Z"
19
+ )
17
20
  _SHA256 = re.compile(r"[0-9a-f]{64}\Z")
18
21
  _OCI_IMAGE = re.compile(
19
22
  r"[a-z0-9][a-z0-9./_-]*(?::[A-Za-z0-9._-]+)?@sha256:[0-9a-f]{64}\Z"
@@ -21,6 +24,7 @@ _OCI_IMAGE = re.compile(
21
24
  _ROOT_KEYS = {
22
25
  "schema_version",
23
26
  "kit_id",
27
+ "seqevi_version",
24
28
  "adapter",
25
29
  "platform",
26
30
  "dbcan_version",
@@ -54,6 +58,7 @@ class KitManifest:
54
58
 
55
59
  schema_version: int
56
60
  kit_id: str
61
+ seqevi_version: str
57
62
  adapter: AdapterName
58
63
  platform: str
59
64
  dbcan_version: str
@@ -105,6 +110,9 @@ def parse_kit_manifest(
105
110
  kit_id = _string(document["kit_id"], "kit_id")
106
111
  if _KIT_ID.fullmatch(kit_id) is None:
107
112
  raise SetupError("managed kit manifest kit_id contains unsafe characters")
113
+ seqevi_version = _string(document["seqevi_version"], "seqevi_version")
114
+ if _SEQEVI_VERSION.fullmatch(seqevi_version) is None:
115
+ raise SetupError("managed kit manifest seqevi_version must be X.Y.Z")
108
116
  adapter_text = _string(document["adapter"], "adapter")
109
117
  try:
110
118
  adapter = AdapterName(adapter_text)
@@ -166,6 +174,7 @@ def parse_kit_manifest(
166
174
  return KitManifest(
167
175
  schema_version=schema_version,
168
176
  kit_id=kit_id,
177
+ seqevi_version=seqevi_version,
169
178
  adapter=adapter,
170
179
  platform=platform,
171
180
  dbcan_version=dbcan_version,
@@ -15,7 +15,6 @@ from urllib.parse import urlsplit
15
15
 
16
16
  import duckdb
17
17
 
18
- from seqevi import __version__
19
18
  from seqevi.annotate import AnnotationMetrics, AnnotationSummary
20
19
  from seqevi.errors import AnnotationError
21
20
  from seqevi.evidence import sha256_digest
@@ -354,7 +353,7 @@ def _validate_result(
354
353
  metadata = dict(zip(columns, row, strict=True))
355
354
  checks = {
356
355
  "ResultFormatVersion": RESULT_FORMAT_VERSION,
357
- "SeqEviVersion": __version__,
356
+ "SeqEviVersion": manifest.seqevi_version,
358
357
  "Adapter": profile.adapter.value,
359
358
  "AdapterContractVersion": "dbcan-cazyme/1",
360
359
  "UpstreamTool": "dbCAN",
@@ -1,10 +1,11 @@
1
1
  schema_version = 1
2
2
  kit_id = "dbcan-cazyme-5.2.9-db-2026.05.05"
3
+ seqevi_version = "0.3.1"
3
4
  adapter = "dbcan-cazyme"
4
5
  platform = "linux/amd64"
5
6
  dbcan_version = "5.2.9"
6
7
  diamond_version = "2.1.15"
7
- image = "ghcr.io/fuqingzh/seqevi-dbcan@sha256:4dc125b7c6ee28ca29a90025478938c1f3958072ff6fd076c8a4df1b9b5c747c"
8
+ image = "ghcr.io/fuqingzh/seqevi-dbcan@sha256:051cbffa57b1b39fa8a6f811832dcc3224671b4a378ef8df7000f7f4314db9b6"
8
9
 
9
10
  [resource]
10
11
  name = "dbcan"