dayhoff-tools 1.14.10__py3-none-any.whl → 1.14.11__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.
@@ -30,12 +30,18 @@ def get_array_index() -> int:
30
30
 
31
31
  For array jobs, reads AWS_BATCH_JOB_ARRAY_INDEX.
32
32
  For retry jobs, maps from BATCH_RETRY_INDICES.
33
+ For resliced retry jobs, uses the raw array index (chunks are renumbered).
33
34
  For single jobs (array_size=1), defaults to 0.
34
35
 
35
36
  Returns:
36
37
  The array index this worker should process
37
38
  """
38
- # Check for retry mode first
39
+ # For resliced retries, use raw array index (chunks are renumbered 0..N-1)
40
+ if os.environ.get("RESLICE_PREFIX"):
41
+ array_idx = os.environ.get("AWS_BATCH_JOB_ARRAY_INDEX", "0")
42
+ return int(array_idx)
43
+
44
+ # Check for retry mode (non-resliced)
39
45
  retry_indices = os.environ.get("BATCH_RETRY_INDICES")
40
46
  if retry_indices:
41
47
  # In retry mode, we have a list of indices and use array index to pick
@@ -69,6 +75,20 @@ def get_job_dir() -> Path:
69
75
  return Path(job_dir)
70
76
 
71
77
 
78
+ def get_reslice_prefix() -> str | None:
79
+ """Get the reslice prefix from environment if set.
80
+
81
+ When RESLICE_PREFIX is set (e.g., 'r1'), files are named like:
82
+ - Input: chunk_r1_000.fasta
83
+ - Output: embed_r1_000.h5
84
+ - Done marker: embed_r1_000.done
85
+
86
+ Returns:
87
+ The reslice prefix or None if not in reslice mode
88
+ """
89
+ return os.environ.get("RESLICE_PREFIX")
90
+
91
+
72
92
  def get_input_file(index: int, job_dir: Path, prefix: str = "chunk") -> Path:
73
93
  """Get the input file path for a given index.
74
94
 
@@ -80,6 +100,9 @@ def get_input_file(index: int, job_dir: Path, prefix: str = "chunk") -> Path:
80
100
  Returns:
81
101
  Path to input file
82
102
  """
103
+ reslice = get_reslice_prefix()
104
+ if reslice:
105
+ return job_dir / "input" / f"{prefix}_{reslice}_{index:03d}.fasta"
83
106
  return job_dir / "input" / f"{prefix}_{index:03d}.fasta"
84
107
 
85
108
 
@@ -97,6 +120,9 @@ def get_output_file(
97
120
  Returns:
98
121
  Path to output file
99
122
  """
123
+ reslice = get_reslice_prefix()
124
+ if reslice:
125
+ return job_dir / "output" / f"{prefix}_{reslice}_{index:03d}{suffix}"
100
126
  return job_dir / "output" / f"{prefix}_{index:03d}{suffix}"
101
127
 
102
128
 
@@ -111,6 +137,9 @@ def get_done_marker(index: int, job_dir: Path, prefix: str = "embed") -> Path:
111
137
  Returns:
112
138
  Path to done marker file
113
139
  """
140
+ reslice = get_reslice_prefix()
141
+ if reslice:
142
+ return job_dir / "output" / f"{prefix}_{reslice}_{index:03d}.done"
114
143
  return job_dir / "output" / f"{prefix}_{index:03d}.done"
115
144
 
116
145
 
@@ -118,7 +118,12 @@ def finalize(job_id, output, force, keep_intermediates, full_output, base_path):
118
118
 
119
119
 
120
120
  def _check_completion(job_id: str, base_path: str) -> list[int]:
121
- """Check which chunks are incomplete (no .done marker)."""
121
+ """Check which chunks are incomplete (no .done marker).
122
+
123
+ Handles both original chunks (chunk_000.fasta) and resliced chunks
124
+ (chunk_r1_000.fasta). For original chunks that were resliced in a retry,
125
+ checks if all resliced chunks completed.
126
+ """
122
127
  job_dir = get_job_dir(job_id, base_path)
123
128
  input_dir = job_dir / "input"
124
129
  output_dir = job_dir / "output"
@@ -126,13 +131,53 @@ def _check_completion(job_id: str, base_path: str) -> list[int]:
126
131
  if not input_dir.exists():
127
132
  return []
128
133
 
134
+ # Load manifest to check for resliced retries
135
+ try:
136
+ manifest = load_manifest(job_id, base_path)
137
+ resliced_indices: set[int] = set()
138
+ reslice_info: dict[str, int] = {} # prefix -> expected count
139
+
140
+ for retry in manifest.retries:
141
+ if retry.reslice_prefix and retry.reslice_count:
142
+ resliced_indices.update(retry.indices)
143
+ reslice_info[retry.reslice_prefix] = retry.reslice_count
144
+ except FileNotFoundError:
145
+ resliced_indices = set()
146
+ reslice_info = {}
147
+
129
148
  incomplete = []
130
- for chunk_path in sorted(input_dir.glob("chunk_*.fasta")):
149
+
150
+ # Check original chunks (chunk_000.fasta pattern)
151
+ for chunk_path in sorted(input_dir.glob("chunk_[0-9][0-9][0-9].fasta")):
131
152
  idx_str = chunk_path.stem.split("_")[1]
132
153
  idx = int(idx_str)
154
+
155
+ # Check for original done marker
133
156
  done_marker = output_dir / f"embed_{idx:03d}.done"
134
- if not done_marker.exists():
135
- incomplete.append(idx)
157
+ if done_marker.exists():
158
+ continue
159
+
160
+ # Check if this chunk was resliced
161
+ if idx in resliced_indices:
162
+ # Find which retry covered this index and check if complete
163
+ is_covered = False
164
+ for retry in manifest.retries:
165
+ if (
166
+ retry.reslice_prefix
167
+ and retry.reslice_count
168
+ and idx in retry.indices
169
+ ):
170
+ # Check if all resliced chunks for this retry completed
171
+ done_count = len(
172
+ list(output_dir.glob(f"embed_{retry.reslice_prefix}_*.done"))
173
+ )
174
+ if done_count >= retry.reslice_count:
175
+ is_covered = True
176
+ break
177
+ if is_covered:
178
+ continue
179
+
180
+ incomplete.append(idx)
136
181
 
137
182
  return incomplete
138
183
 
@@ -1,6 +1,7 @@
1
1
  """Retry command for re-running failed chunks."""
2
2
 
3
3
  from datetime import datetime
4
+ from pathlib import Path
4
5
 
5
6
  import click
6
7
 
@@ -19,20 +20,37 @@ from ..manifest import (
19
20
  @click.command()
20
21
  @click.argument("job_id")
21
22
  @click.option("--indices", help="Specific indices to retry (comma-separated)")
23
+ @click.option(
24
+ "--queue",
25
+ help="Override job queue (e.g., 't4-1x' for on-demand instead of spot)",
26
+ )
27
+ @click.option(
28
+ "--reslice",
29
+ type=int,
30
+ help="Reslice failed chunks into N thinner chunks (reduces interruption risk)",
31
+ )
22
32
  @click.option(
23
33
  "--dry-run", is_flag=True, help="Show what would be retried without submitting"
24
34
  )
25
35
  @click.option("--base-path", default=BATCH_JOBS_BASE, help="Base path for job data")
26
- def retry(job_id, indices, dry_run, base_path):
36
+ def retry(job_id, indices, queue, reslice, dry_run, base_path):
27
37
  """Retry failed chunks of a batch job.
28
38
 
29
39
  Identifies failed array indices and submits a new job to retry only
30
- those specific indices.
40
+ those specific indices. Outputs go to the same job directory, so
41
+ finalization works normally after retries complete.
42
+
43
+ The --reslice option concatenates failed chunks and re-splits them into
44
+ thinner slices, reducing the time per worker and thus the risk of spot
45
+ interruptions. Resliced outputs are named with a prefix (e.g., embed_r1_000.h5)
46
+ and are automatically included in finalization.
31
47
 
32
48
  \b
33
49
  Examples:
34
50
  dh batch retry dma-embed-20260109-a3f2 # Retry all failed
35
51
  dh batch retry dma-embed-20260109-a3f2 --indices 5,12,27 # Retry specific indices
52
+ dh batch retry dma-embed-20260109-a3f2 --queue t4-1x # Use on-demand (no spot interruptions)
53
+ dh batch retry dma-embed-20260109-a3f2 --reslice 40 # Reslice into 40 thinner chunks
36
54
  dh batch retry dma-embed-20260109-a3f2 --dry-run # Show what would be retried
37
55
  """
38
56
  # Load manifest
@@ -56,48 +74,90 @@ def retry(job_id, indices, dry_run, base_path):
56
74
 
57
75
  click.echo(f"Found {len(retry_indices)} chunks to retry: {retry_indices}")
58
76
 
59
- if dry_run:
60
- click.echo()
61
- click.echo(click.style("Dry run - job not submitted", fg="yellow"))
62
- return
63
-
64
77
  # Check if we have the required info
65
78
  if not manifest.batch:
66
79
  click.echo("Job has no batch configuration.", err=True)
67
80
  raise SystemExit(1)
68
81
 
69
- # Generate retry job ID
70
- retry_id = f"{job_id}-r{len(manifest.retries) + 1}"
82
+ # Generate retry job ID and reslice prefix
83
+ retry_num = len(manifest.retries) + 1
84
+ retry_id = f"{job_id}-r{retry_num}"
85
+ reslice_prefix = f"r{retry_num}" if reslice else None
86
+
87
+ job_dir = get_job_dir(job_id, base_path)
88
+
89
+ if reslice:
90
+ # Count sequences in failed chunks to estimate split
91
+ total_seqs = _count_sequences_in_chunks(job_dir, retry_indices)
92
+ seqs_per_chunk = max(1, total_seqs // reslice)
93
+ click.echo(f"Total sequences in failed chunks: {total_seqs:,}")
94
+ click.echo(f"Reslicing into {reslice} chunks (~{seqs_per_chunk:,} seqs each)")
95
+
96
+ if dry_run:
97
+ click.echo()
98
+ click.echo(click.style("Dry run - job not submitted", fg="yellow"))
99
+ return
71
100
 
72
101
  click.echo()
73
102
  click.echo(f"Retry job ID: {retry_id}")
74
103
 
104
+ # Handle reslicing if requested
105
+ if reslice:
106
+ click.echo(f"Reslice prefix: {reslice_prefix}")
107
+ actual_chunks = _reslice_failed_chunks(
108
+ job_dir, retry_indices, reslice_prefix, reslice
109
+ )
110
+ click.echo(f"Created {actual_chunks} resliced chunks")
111
+ array_size = actual_chunks
112
+ else:
113
+ array_size = len(retry_indices)
114
+
75
115
  # Submit retry job
76
116
  try:
77
117
  client = BatchClient()
78
- job_dir = get_job_dir(job_id, base_path)
79
118
 
80
119
  environment = {
81
120
  "JOB_DIR": str(job_dir),
82
121
  "JOB_ID": job_id,
83
- "BATCH_RETRY_INDICES": ",".join(str(i) for i in retry_indices),
84
122
  }
85
123
 
86
- batch_job_id = client.submit_array_job_with_indices(
87
- job_name=retry_id,
88
- job_definition=manifest.batch.job_definition or "dayhoff-embed-t5",
89
- job_queue=manifest.batch.queue,
90
- indices=retry_indices,
91
- environment=environment,
92
- timeout_seconds=6 * 3600,
93
- retry_attempts=3,
94
- )
124
+ # Use provided queue or fall back to original
125
+ job_queue = queue or manifest.batch.queue
126
+ if queue and queue != manifest.batch.queue:
127
+ click.echo(f"Using queue: {job_queue} (original: {manifest.batch.queue})")
128
+
129
+ if reslice:
130
+ # Resliced retry: use RESLICE_PREFIX, sequential indices 0..N-1
131
+ environment["RESLICE_PREFIX"] = reslice_prefix
132
+ batch_job_id = client.submit_job(
133
+ job_name=retry_id,
134
+ job_definition=manifest.batch.job_definition or "dayhoff-embed-t5",
135
+ job_queue=job_queue,
136
+ array_size=array_size,
137
+ environment=environment,
138
+ timeout_seconds=6 * 3600,
139
+ retry_attempts=5,
140
+ )
141
+ else:
142
+ # Standard retry: use BATCH_RETRY_INDICES mapping
143
+ environment["BATCH_RETRY_INDICES"] = ",".join(str(i) for i in retry_indices)
144
+ batch_job_id = client.submit_array_job_with_indices(
145
+ job_name=retry_id,
146
+ job_definition=manifest.batch.job_definition or "dayhoff-embed-t5",
147
+ job_queue=job_queue,
148
+ indices=retry_indices,
149
+ environment=environment,
150
+ timeout_seconds=6 * 3600,
151
+ retry_attempts=5,
152
+ )
95
153
 
96
154
  # Update manifest with retry info
97
155
  retry_info = RetryInfo(
98
156
  retry_id=retry_id,
99
157
  indices=retry_indices,
100
158
  batch_job_id=batch_job_id,
159
+ reslice_prefix=reslice_prefix,
160
+ reslice_count=array_size if reslice else None,
101
161
  created=datetime.utcnow(),
102
162
  )
103
163
  manifest.retries.append(retry_info)
@@ -129,8 +189,8 @@ def _find_incomplete_chunks(job_id: str, base_path: str) -> list[int]:
129
189
  if not input_dir.exists():
130
190
  return []
131
191
 
132
- # Find all input chunks
133
- input_chunks = sorted(input_dir.glob("chunk_*.fasta"))
192
+ # Find all original input chunks (not resliced ones)
193
+ input_chunks = sorted(input_dir.glob("chunk_[0-9][0-9][0-9].fasta"))
134
194
  incomplete = []
135
195
 
136
196
  for chunk_path in input_chunks:
@@ -144,3 +204,85 @@ def _find_incomplete_chunks(job_id: str, base_path: str) -> list[int]:
144
204
  incomplete.append(idx)
145
205
 
146
206
  return incomplete
207
+
208
+
209
+ def _count_sequences_in_chunks(job_dir: Path, indices: list[int]) -> int:
210
+ """Count total sequences in the specified chunk files."""
211
+ input_dir = job_dir / "input"
212
+ total = 0
213
+
214
+ for idx in indices:
215
+ chunk_path = input_dir / f"chunk_{idx:03d}.fasta"
216
+ if chunk_path.exists():
217
+ with open(chunk_path) as f:
218
+ for line in f:
219
+ if line.startswith(">"):
220
+ total += 1
221
+
222
+ return total
223
+
224
+
225
+ def _reslice_failed_chunks(
226
+ job_dir: Path, indices: list[int], reslice_prefix: str, num_chunks: int
227
+ ) -> int:
228
+ """Concatenate failed chunks and re-split into thinner slices.
229
+
230
+ Creates new chunk files named chunk_{prefix}_000.fasta, etc.
231
+
232
+ Args:
233
+ job_dir: Job directory path
234
+ indices: List of failed chunk indices
235
+ reslice_prefix: Prefix for new chunk files (e.g., 'r1')
236
+ num_chunks: Target number of new chunks
237
+
238
+ Returns:
239
+ Actual number of chunks created
240
+ """
241
+ from dayhoff_tools.fasta import split_fasta
242
+ import tempfile
243
+
244
+ input_dir = job_dir / "input"
245
+
246
+ # Concatenate all failed chunks into a temp file
247
+ with tempfile.NamedTemporaryFile(
248
+ mode="w", suffix=".fasta", delete=False
249
+ ) as tmp_file:
250
+ tmp_path = tmp_file.name
251
+ total_seqs = 0
252
+
253
+ for idx in indices:
254
+ chunk_path = input_dir / f"chunk_{idx:03d}.fasta"
255
+ if chunk_path.exists():
256
+ with open(chunk_path) as f:
257
+ for line in f:
258
+ tmp_file.write(line)
259
+ if line.startswith(">"):
260
+ total_seqs += 1
261
+
262
+ try:
263
+ # Calculate sequences per chunk
264
+ seqs_per_chunk = max(1, (total_seqs + num_chunks - 1) // num_chunks)
265
+
266
+ # Split into new chunks with reslice prefix
267
+ # split_fasta creates files like: chunk_r1_1.fasta, chunk_r1_2.fasta, etc.
268
+ actual_chunks = split_fasta(
269
+ fasta_file=tmp_path,
270
+ target_folder=str(input_dir),
271
+ base_name=f"chunk_{reslice_prefix}",
272
+ sequences_per_file=seqs_per_chunk,
273
+ max_files=num_chunks,
274
+ show_progress=True,
275
+ )
276
+
277
+ # Rename to zero-padded indices (chunk_r1_000.fasta, etc.)
278
+ for i in range(1, actual_chunks + 1):
279
+ old_name = input_dir / f"chunk_{reslice_prefix}_{i}.fasta"
280
+ new_name = input_dir / f"chunk_{reslice_prefix}_{i-1:03d}.fasta"
281
+ if old_name.exists():
282
+ old_name.rename(new_name)
283
+
284
+ return actual_chunks
285
+
286
+ finally:
287
+ # Clean up temp file
288
+ Path(tmp_path).unlink(missing_ok=True)
@@ -62,6 +62,12 @@ class RetryInfo(BaseModel):
62
62
  retry_id: str = Field(..., description="Retry job ID")
63
63
  indices: list[int] = Field(..., description="Array indices being retried")
64
64
  batch_job_id: str | None = Field(None, description="AWS Batch job ID for retry")
65
+ reslice_prefix: str | None = Field(
66
+ None, description="Reslice prefix if chunks were resliced (e.g., 'r1')"
67
+ )
68
+ reslice_count: int | None = Field(
69
+ None, description="Number of resliced chunks created"
70
+ )
65
71
  created: datetime = Field(default_factory=datetime.utcnow)
66
72
 
67
73
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dayhoff-tools
3
- Version: 1.14.10
3
+ Version: 1.14.11
4
4
  Summary: Common tools for all the repos at Dayhoff Labs
5
5
  Author: Daniel Martin-Alarcon
6
6
  Author-email: dma@dayhofflabs.com
@@ -1,7 +1,7 @@
1
1
  dayhoff_tools/__init__.py,sha256=M5zThPyEBRYa5CfwlzKhcqTevWn3OKu62cjV6Zqie2A,469
2
2
  dayhoff_tools/batch/__init__.py,sha256=N7v1pUg3rp68W6J7cX2Gssxdfp57Z7G0WeFJh7gKeiM,163
3
3
  dayhoff_tools/batch/workers/__init__.py,sha256=DJTtiBSE5k9J6qwG_MsXDn8hgz_lvEdaMwqKa6YyqXo,369
4
- dayhoff_tools/batch/workers/base.py,sha256=Jie5ScJrPSRkmLQdcM2hPJTmk3pIeLL0oA8Y9pkP0HA,4269
4
+ dayhoff_tools/batch/workers/base.py,sha256=ZveUds6GW-aInj9LL0_VEYkXxOyZWRcOeJdacnEb8xo,5346
5
5
  dayhoff_tools/batch/workers/boltz.py,sha256=breCGITkX-_Xj9I_C1R-mKkG9RUCzSw3Kcdbbx_S5LA,18405
6
6
  dayhoff_tools/batch/workers/embed_t5.py,sha256=A5WqsQa1WZ7_la5X5wt0XUP-VwOglH04WyEINhwipeY,2750
7
7
  dayhoff_tools/chemistry/standardizer.py,sha256=uMn7VwHnx02nc404eO6fRuS4rsl4dvSPf2ElfZDXEpY,11188
@@ -14,15 +14,15 @@ dayhoff_tools/cli/batch/commands/boltz.py,sha256=N0LksmtOpkvnEsR0SAUHxtksKPAsQjR
14
14
  dayhoff_tools/cli/batch/commands/cancel.py,sha256=ZnHAJVzMGC0_1EQGpMSdYUlzm9yi-E9NxRJKBsetYW8,3111
15
15
  dayhoff_tools/cli/batch/commands/clean.py,sha256=nWOKbVM2nDuLMpyC038Q9aylOQxk2bq4N0JF65qJg-s,4570
16
16
  dayhoff_tools/cli/batch/commands/embed_t5.py,sha256=QXFydAw0wndevdzXF1cxikxMmvn1BuQ5p9lwutQFajU,11453
17
- dayhoff_tools/cli/batch/commands/finalize.py,sha256=Ir8XeA62lk_nUcWCLjjsYeqS58BBM1LTaEorEmQxOhA,11118
17
+ dayhoff_tools/cli/batch/commands/finalize.py,sha256=xr3GFcMbvtU6UYiJI3UXhQqeaACSFzKIZOxz4GK-Dmo,12785
18
18
  dayhoff_tools/cli/batch/commands/list_jobs.py,sha256=COfxZddDVUAHeTayNAB3ruYNhgrE3osgFxY2qzf33cg,4284
19
19
  dayhoff_tools/cli/batch/commands/local.py,sha256=dZeKhNakaM1jS-EoByAwg1nWspRRoOmYzcwzjEKBaIA,3226
20
20
  dayhoff_tools/cli/batch/commands/logs.py,sha256=ctgJksdzFmqBdD18ePPsZe2BpuJYtHz2xAaMPnUplmQ,5293
21
- dayhoff_tools/cli/batch/commands/retry.py,sha256=PbstKr7iP7Uu4csisp3cvM8al9YOFw27XVlc591jLbE,4509
21
+ dayhoff_tools/cli/batch/commands/retry.py,sha256=JgOMiwuESNG4Wp_fQ_vxy1RwyOZPt_kmVbLYdxYTVBY,9897
22
22
  dayhoff_tools/cli/batch/commands/status.py,sha256=k8ltke62vKUX_4MmnFDEd6H2s8KmVFA71UUXLbleA7o,9790
23
23
  dayhoff_tools/cli/batch/commands/submit.py,sha256=AXbvSReN8fLlzR5swE81pH7yvbCC1GUMCbsDrfoHAws,6786
24
24
  dayhoff_tools/cli/batch/job_id.py,sha256=mqr8SwcPlUWIYLaR_C4kACmL2ZFK8jaddd7B-45-XaQ,4246
25
- dayhoff_tools/cli/batch/manifest.py,sha256=blUw2ASxFTmdLV5ikz4VD91n5CN0V37rlyX11M0H2VE,8624
25
+ dayhoff_tools/cli/batch/manifest.py,sha256=eLyiOFXonAUh5rfHcXyR2CzmRKXQz9-tTbJcWnVbbmE,8857
26
26
  dayhoff_tools/cli/cloud_commands.py,sha256=xrWQZm48e09GTh8RmYh3JqXUzW8mMy07dHk_77LqZx8,41199
27
27
  dayhoff_tools/cli/engine1/__init__.py,sha256=RE45X2IPCNUvQp6_OHI2lbjwoqexkH3roUPccIHkeJo,9540
28
28
  dayhoff_tools/cli/engine1/engine_core.py,sha256=TECO6766GCZTtbxOY7QRggwmjpMH9JZSuxjbwF46unU,27061
@@ -71,7 +71,7 @@ dayhoff_tools/intake/uniprot.py,sha256=BZYJQF63OtPcBBnQ7_P9gulxzJtqyorgyuDiPeOJq
71
71
  dayhoff_tools/logs.py,sha256=DKdeP0k0kliRcilwvX0mUB2eipO5BdWUeHwh-VnsICs,838
72
72
  dayhoff_tools/sqlite.py,sha256=jV55ikF8VpTfeQqqlHSbY8OgfyfHj8zgHNpZjBLos_E,18672
73
73
  dayhoff_tools/warehouse.py,sha256=UETBtZD3r7WgvURqfGbyHlT7cxoiVq8isjzMuerKw8I,24475
74
- dayhoff_tools-1.14.10.dist-info/METADATA,sha256=fhnkPfMm1G4qVxsu1zrGjLweThYGZpOh8XU61le3zVQ,3185
75
- dayhoff_tools-1.14.10.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
76
- dayhoff_tools-1.14.10.dist-info/entry_points.txt,sha256=iAf4jteNqW3cJm6CO6czLxjW3vxYKsyGLZ8WGmxamSc,49
77
- dayhoff_tools-1.14.10.dist-info/RECORD,,
74
+ dayhoff_tools-1.14.11.dist-info/METADATA,sha256=-3MjX0CiSeDoQ0MiF0mQlw700sWzZgeyVowtbljZ5qc,3185
75
+ dayhoff_tools-1.14.11.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
76
+ dayhoff_tools-1.14.11.dist-info/entry_points.txt,sha256=iAf4jteNqW3cJm6CO6czLxjW3vxYKsyGLZ8WGmxamSc,49
77
+ dayhoff_tools-1.14.11.dist-info/RECORD,,