dayhoff-tools 1.14.7__py3-none-any.whl → 1.14.8__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.
@@ -12,6 +12,7 @@ import click
12
12
 
13
13
  from .commands.boltz import boltz
14
14
  from .commands.cancel import cancel
15
+ from .commands.clean import clean
15
16
  from .commands.embed_t5 import embed_t5
16
17
  from .commands.finalize import finalize
17
18
  from .commands.list_jobs import list_jobs
@@ -36,6 +37,7 @@ def batch_cli():
36
37
  finalize Combine results and clean up
37
38
  local Run a chunk locally for debugging
38
39
  list List recent jobs
40
+ clean Remove old completed job directories
39
41
 
40
42
  \b
41
43
  Embedding Pipelines:
@@ -77,6 +79,7 @@ batch_cli.add_command(retry)
77
79
  batch_cli.add_command(finalize)
78
80
  batch_cli.add_command(local)
79
81
  batch_cli.add_command(list_jobs, name="list")
82
+ batch_cli.add_command(clean)
80
83
 
81
84
  # Register pipeline commands
82
85
  batch_cli.add_command(embed_t5, name="embed-t5")
@@ -242,6 +242,64 @@ class BatchClient:
242
242
  failed=status_summary.get("FAILED", 0),
243
243
  )
244
244
 
245
+ def get_job_statuses_batch(self, job_ids: list[str]) -> dict[str, str]:
246
+ """Get status for multiple jobs in a single API call.
247
+
248
+ AWS Batch allows up to 100 job IDs per describe_jobs call.
249
+ This method handles batching for larger lists.
250
+
251
+ Args:
252
+ job_ids: List of AWS Batch job IDs
253
+
254
+ Returns:
255
+ Dictionary mapping job_id -> status string
256
+ Status will be one of: SUBMITTED, PENDING, RUNNABLE, STARTING,
257
+ RUNNING, SUCCEEDED, FAILED, or "UNKNOWN" if not found.
258
+ For array jobs, derives overall status from child statuses.
259
+ """
260
+ if not job_ids:
261
+ return {}
262
+
263
+ results = {}
264
+ batch_size = 100 # AWS Batch limit
265
+
266
+ for i in range(0, len(job_ids), batch_size):
267
+ batch = job_ids[i : i + batch_size]
268
+ try:
269
+ response = self.batch.describe_jobs(jobs=batch)
270
+ for job in response.get("jobs", []):
271
+ job_id = job.get("jobId")
272
+ status = job.get("status", "UNKNOWN")
273
+
274
+ # For array jobs, derive overall status from children
275
+ if "arrayProperties" in job:
276
+ summary = job["arrayProperties"].get("statusSummary", {})
277
+ total = job["arrayProperties"].get("size", 0)
278
+ succeeded = summary.get("SUCCEEDED", 0)
279
+ failed = summary.get("FAILED", 0)
280
+
281
+ if succeeded + failed == total:
282
+ # All children complete
283
+ status = "SUCCEEDED" if failed == 0 else "FAILED"
284
+ elif summary.get("RUNNING", 0) > 0:
285
+ status = "RUNNING"
286
+ elif summary.get("STARTING", 0) > 0:
287
+ status = "STARTING"
288
+ elif summary.get("RUNNABLE", 0) > 0:
289
+ status = "RUNNABLE"
290
+ elif summary.get("PENDING", 0) > 0:
291
+ status = "PENDING"
292
+
293
+ results[job_id] = status
294
+ except ClientError as e:
295
+ logger.warning(f"Failed to describe batch of jobs: {e}")
296
+ # Mark these as unknown
297
+ for job_id in batch:
298
+ if job_id not in results:
299
+ results[job_id] = "UNKNOWN"
300
+
301
+ return results
302
+
245
303
  def get_failed_indices(self, job_id: str) -> list[int]:
246
304
  """Get the array indices that failed for an array job.
247
305
 
@@ -0,0 +1,139 @@
1
+ """Clean command for removing old job directories."""
2
+
3
+ import click
4
+
5
+ from ..aws_batch import BatchClient, BatchError
6
+ from ..manifest import (
7
+ BATCH_JOBS_BASE,
8
+ JobStatus,
9
+ delete_job_directory,
10
+ list_jobs as list_manifests,
11
+ )
12
+ from .status import format_time_ago, _aws_status_to_job_status
13
+
14
+
15
+ @click.command("clean")
16
+ @click.option("--user", help="Only clean jobs for this user")
17
+ @click.option(
18
+ "--older-than",
19
+ type=int,
20
+ default=7,
21
+ help="Only clean jobs older than N days [default: 7]",
22
+ )
23
+ @click.option("--dry-run", is_flag=True, help="Show what would be cleaned without deleting")
24
+ @click.option("--force", is_flag=True, help="Delete without confirmation")
25
+ @click.option("--base-path", default=BATCH_JOBS_BASE, help="Base path for job data")
26
+ def clean(user, older_than, dry_run, force, base_path):
27
+ """Remove completed job directories to free up space.
28
+
29
+ Only removes jobs that have SUCCEEDED or FAILED in AWS Batch.
30
+ Jobs that are still running or pending are never removed.
31
+
32
+ \b
33
+ Examples:
34
+ dh batch clean # Clean jobs older than 7 days
35
+ dh batch clean --older-than 1 # Clean jobs older than 1 day
36
+ dh batch clean --dry-run # Show what would be cleaned
37
+ dh batch clean --user dma # Only clean dma's jobs
38
+ """
39
+ from datetime import datetime, timedelta, timezone
40
+
41
+ cutoff = datetime.now(timezone.utc) - timedelta(days=older_than)
42
+
43
+ # Get all manifests
44
+ manifests = list_manifests(
45
+ base_path=base_path,
46
+ user=user,
47
+ status=None,
48
+ pipeline=None,
49
+ limit=500,
50
+ )
51
+
52
+ if not manifests:
53
+ click.echo("No jobs found.")
54
+ return
55
+
56
+ # Filter to old jobs
57
+ old_manifests = []
58
+ for m in manifests:
59
+ created = m.created
60
+ if created.tzinfo is None:
61
+ created = created.replace(tzinfo=timezone.utc)
62
+ if created < cutoff:
63
+ old_manifests.append(m)
64
+
65
+ if not old_manifests:
66
+ click.echo(f"No jobs older than {older_than} days found.")
67
+ return
68
+
69
+ # Get live statuses for old jobs
70
+ batch_job_ids = []
71
+ manifest_to_batch_id = {}
72
+ for m in old_manifests:
73
+ if m.batch and m.batch.job_id:
74
+ batch_job_ids.append(m.batch.job_id)
75
+ manifest_to_batch_id[m.job_id] = m.batch.job_id
76
+
77
+ live_statuses = {}
78
+ if batch_job_ids:
79
+ try:
80
+ client = BatchClient()
81
+ live_statuses = client.get_job_statuses_batch(batch_job_ids)
82
+ except BatchError as e:
83
+ click.echo(f"Error: Could not fetch status from AWS Batch: {e}")
84
+ click.echo("Cannot safely clean jobs without knowing their status.")
85
+ return
86
+
87
+ # Find jobs that are safe to clean (SUCCEEDED or FAILED)
88
+ safe_to_clean = []
89
+ for manifest in old_manifests:
90
+ if manifest.job_id in manifest_to_batch_id:
91
+ batch_id = manifest_to_batch_id[manifest.job_id]
92
+ aws_status = live_statuses.get(batch_id, "UNKNOWN")
93
+ if aws_status in ("SUCCEEDED", "FAILED"):
94
+ safe_to_clean.append((manifest, aws_status))
95
+ elif manifest.status in (JobStatus.FINALIZED, JobStatus.CANCELLED):
96
+ # Already finalized or cancelled - safe to clean
97
+ safe_to_clean.append((manifest, manifest.status.value.upper()))
98
+
99
+ if not safe_to_clean:
100
+ click.echo(f"No completed jobs older than {older_than} days to clean.")
101
+ return
102
+
103
+ # Show what will be cleaned
104
+ click.echo()
105
+ click.echo(f"{'JOB ID':<35} {'STATUS':<12} {'CREATED':<12}")
106
+ click.echo("-" * 65)
107
+
108
+ for manifest, status in safe_to_clean:
109
+ click.echo(
110
+ f"{manifest.job_id:<35} "
111
+ f"{status:<12} "
112
+ f"{format_time_ago(manifest.created):<12}"
113
+ )
114
+
115
+ click.echo()
116
+ click.echo(f"Found {len(safe_to_clean)} completed jobs to clean.")
117
+
118
+ if dry_run:
119
+ click.echo("(dry-run: no changes made)")
120
+ return
121
+
122
+ # Confirm before deleting
123
+ if not force:
124
+ if not click.confirm("Delete these job directories?"):
125
+ click.echo("Cancelled.")
126
+ return
127
+
128
+ # Delete job directories
129
+ deleted = 0
130
+ for manifest, _ in safe_to_clean:
131
+ try:
132
+ delete_job_directory(manifest.job_id, base_path)
133
+ deleted += 1
134
+ click.echo(f" Deleted: {manifest.job_id}")
135
+ except Exception as e:
136
+ click.echo(f" Failed to delete {manifest.job_id}: {e}")
137
+
138
+ click.echo()
139
+ click.echo(f"Cleaned {deleted} job directories.")
@@ -2,8 +2,9 @@
2
2
 
3
3
  import click
4
4
 
5
+ from ..aws_batch import BatchClient, BatchError
5
6
  from ..manifest import BATCH_JOBS_BASE, JobStatus, list_jobs as list_manifests
6
- from .status import format_status, format_time_ago
7
+ from .status import format_status, format_time_ago, _aws_status_to_job_status
7
8
 
8
9
 
9
10
  @click.command("list")
@@ -23,6 +24,7 @@ def list_jobs(user, status_filter, pipeline, limit, base_path):
23
24
  """List recent batch jobs.
24
25
 
25
26
  Shows a table of recent jobs with their status, pipeline type, and creation time.
27
+ Status is fetched live from AWS Batch.
26
28
 
27
29
  \b
28
30
  Examples:
@@ -34,12 +36,13 @@ def list_jobs(user, status_filter, pipeline, limit, base_path):
34
36
  """
35
37
  status_enum = JobStatus(status_filter) if status_filter else None
36
38
 
39
+ # Fetch more manifests than requested to allow filtering by live status
37
40
  manifests = list_manifests(
38
41
  base_path=base_path,
39
42
  user=user,
40
- status=status_enum,
43
+ status=None, # Don't filter by status yet - will filter after getting live status
41
44
  pipeline=pipeline,
42
- limit=limit,
45
+ limit=limit * 3, # Fetch extra to account for status filtering
43
46
  )
44
47
 
45
48
  if not manifests:
@@ -48,6 +51,51 @@ def list_jobs(user, status_filter, pipeline, limit, base_path):
48
51
  click.echo("Try removing filters to see all jobs.")
49
52
  return
50
53
 
54
+ # Collect AWS Batch job IDs for live status lookup
55
+ batch_job_ids = []
56
+ manifest_to_batch_id = {}
57
+ for m in manifests:
58
+ if m.batch and m.batch.job_id:
59
+ batch_job_ids.append(m.batch.job_id)
60
+ manifest_to_batch_id[m.job_id] = m.batch.job_id
61
+
62
+ # Fetch live statuses from AWS Batch
63
+ live_statuses = {}
64
+ if batch_job_ids:
65
+ try:
66
+ client = BatchClient()
67
+ live_statuses = client.get_job_statuses_batch(batch_job_ids)
68
+ except BatchError as e:
69
+ click.echo(f"Warning: Could not fetch live status from AWS Batch: {e}")
70
+
71
+ # Build display data with live status
72
+ display_data = []
73
+ for manifest in manifests:
74
+ # Use live status if available, otherwise fall back to manifest status
75
+ if manifest.job_id in manifest_to_batch_id:
76
+ batch_id = manifest_to_batch_id[manifest.job_id]
77
+ aws_status = live_statuses.get(batch_id)
78
+ if aws_status:
79
+ live_status = _aws_status_to_job_status(aws_status)
80
+ else:
81
+ live_status = manifest.status
82
+ else:
83
+ live_status = manifest.status
84
+
85
+ # Apply status filter if specified
86
+ if status_enum and live_status != status_enum:
87
+ continue
88
+
89
+ display_data.append((manifest, live_status))
90
+
91
+ # Stop once we have enough
92
+ if len(display_data) >= limit:
93
+ break
94
+
95
+ if not display_data:
96
+ click.echo("No jobs found matching filters.")
97
+ return
98
+
51
99
  # Print header
52
100
  click.echo()
53
101
  click.echo(
@@ -55,17 +103,17 @@ def list_jobs(user, status_filter, pipeline, limit, base_path):
55
103
  )
56
104
  click.echo("-" * 85)
57
105
 
58
- for manifest in manifests:
106
+ for manifest, live_status in display_data:
59
107
  click.echo(
60
108
  f"{manifest.job_id:<35} "
61
- f"{format_status(manifest.status):<21} " # Extra space for ANSI color codes
109
+ f"{format_status(live_status):<21} " # Extra space for ANSI color codes
62
110
  f"{manifest.pipeline:<12} "
63
111
  f"{manifest.user:<10} "
64
112
  f"{format_time_ago(manifest.created):<12}"
65
113
  )
66
114
 
67
115
  click.echo()
68
- click.echo(f"Showing {len(manifests)} jobs.")
116
+ click.echo(f"Showing {len(display_data)} jobs.")
69
117
 
70
118
  # Show filter hints
71
119
  hints = []
@@ -80,21 +80,79 @@ def status(job_id, user, status_filter, pipeline, base_path):
80
80
  _show_job_list(user, status_filter, pipeline, base_path)
81
81
 
82
82
 
83
+ def _aws_status_to_job_status(aws_status: str) -> JobStatus:
84
+ """Convert AWS Batch status to JobStatus enum."""
85
+ mapping = {
86
+ "SUBMITTED": JobStatus.SUBMITTED,
87
+ "PENDING": JobStatus.PENDING,
88
+ "RUNNABLE": JobStatus.RUNNING, # Runnable means waiting for compute
89
+ "STARTING": JobStatus.RUNNING,
90
+ "RUNNING": JobStatus.RUNNING,
91
+ "SUCCEEDED": JobStatus.SUCCEEDED,
92
+ "FAILED": JobStatus.FAILED,
93
+ }
94
+ return mapping.get(aws_status, JobStatus.SUBMITTED)
95
+
96
+
83
97
  def _show_job_list(user, status_filter, pipeline, base_path):
84
98
  """Show a list of recent jobs."""
85
99
  status_enum = JobStatus(status_filter) if status_filter else None
86
100
  manifests = list_manifests(
87
101
  base_path=base_path,
88
102
  user=user,
89
- status=status_enum,
103
+ status=None, # Don't filter yet - we'll filter after getting live status
90
104
  pipeline=pipeline,
91
- limit=20,
105
+ limit=50, # Fetch more, filter later
92
106
  )
93
107
 
94
108
  if not manifests:
95
109
  click.echo("No jobs found.")
96
110
  return
97
111
 
112
+ # Collect AWS Batch job IDs for live status lookup
113
+ batch_job_ids = []
114
+ manifest_to_batch_id = {}
115
+ for m in manifests:
116
+ if m.batch and m.batch.job_id:
117
+ batch_job_ids.append(m.batch.job_id)
118
+ manifest_to_batch_id[m.job_id] = m.batch.job_id
119
+
120
+ # Fetch live statuses from AWS Batch
121
+ live_statuses = {}
122
+ if batch_job_ids:
123
+ try:
124
+ client = BatchClient()
125
+ live_statuses = client.get_job_statuses_batch(batch_job_ids)
126
+ except BatchError as e:
127
+ click.echo(f"Warning: Could not fetch live status from AWS Batch: {e}")
128
+
129
+ # Build display data with live status
130
+ display_data = []
131
+ for manifest in manifests:
132
+ # Use live status if available, otherwise fall back to manifest status
133
+ if manifest.job_id in manifest_to_batch_id:
134
+ batch_id = manifest_to_batch_id[manifest.job_id]
135
+ aws_status = live_statuses.get(batch_id)
136
+ if aws_status:
137
+ live_status = _aws_status_to_job_status(aws_status)
138
+ else:
139
+ live_status = manifest.status
140
+ else:
141
+ live_status = manifest.status
142
+
143
+ # Apply status filter if specified
144
+ if status_enum and live_status != status_enum:
145
+ continue
146
+
147
+ display_data.append((manifest, live_status))
148
+
149
+ if not display_data:
150
+ click.echo("No jobs found matching filters.")
151
+ return
152
+
153
+ # Limit to 20 after filtering
154
+ display_data = display_data[:20]
155
+
98
156
  # Print header
99
157
  click.echo()
100
158
  click.echo(
@@ -102,17 +160,17 @@ def _show_job_list(user, status_filter, pipeline, base_path):
102
160
  )
103
161
  click.echo("-" * 85)
104
162
 
105
- for manifest in manifests:
163
+ for manifest, live_status in display_data:
106
164
  click.echo(
107
165
  f"{manifest.job_id:<35} "
108
- f"{format_status(manifest.status):<21} " # Extra space for color codes
166
+ f"{format_status(live_status):<21} " # Extra space for color codes
109
167
  f"{manifest.pipeline:<12} "
110
168
  f"{manifest.user:<10} "
111
169
  f"{format_time_ago(manifest.created):<12}"
112
170
  )
113
171
 
114
172
  click.echo()
115
- click.echo(f"Showing {len(manifests)} most recent jobs.")
173
+ click.echo(f"Showing {len(display_data)} most recent jobs.")
116
174
  click.echo("Use 'dh batch status <job-id>' for details.")
117
175
 
118
176
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dayhoff-tools
3
- Version: 1.14.7
3
+ Version: 1.14.8
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
@@ -7,18 +7,19 @@ dayhoff_tools/batch/workers/embed_t5.py,sha256=A5WqsQa1WZ7_la5X5wt0XUP-VwOglH04W
7
7
  dayhoff_tools/chemistry/standardizer.py,sha256=uMn7VwHnx02nc404eO6fRuS4rsl4dvSPf2ElfZDXEpY,11188
8
8
  dayhoff_tools/chemistry/utils.py,sha256=jt-7JgF-GeeVC421acX-bobKbLU_X94KNOW24p_P-_M,2257
9
9
  dayhoff_tools/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- dayhoff_tools/cli/batch/__init__.py,sha256=gEuiDPWio8ihrIfF_Q6-5hKOnliPTxDKQrKN8-4Y3Ac,2320
11
- dayhoff_tools/cli/batch/aws_batch.py,sha256=IMnNBXmyfQ1GkBqWV9OWYNNjxFNDXE_tv8sUWq9n1E8,12727
10
+ dayhoff_tools/cli/batch/__init__.py,sha256=0QMxkelGUdvdd7-P-WG8YX2h0qjkKKb-0JbEHGrH7pE,2437
11
+ dayhoff_tools/cli/batch/aws_batch.py,sha256=DOp8KvmTrie15O61DP1HT13PSr3s5imxM-VZHvaLO7Q,15174
12
12
  dayhoff_tools/cli/batch/commands/__init__.py,sha256=1xRzzL_mc1hz1Pv0OWNr-g6fkL5XbEsOTGHzrqddLCA,458
13
13
  dayhoff_tools/cli/batch/commands/boltz.py,sha256=KbBxeF0HS_ysgd7MyeJgrkcGTAXChJLQxOhvJ14SxjY,11989
14
14
  dayhoff_tools/cli/batch/commands/cancel.py,sha256=ZnHAJVzMGC0_1EQGpMSdYUlzm9yi-E9NxRJKBsetYW8,3111
15
+ dayhoff_tools/cli/batch/commands/clean.py,sha256=v8R_YtsdkfPjrFmo7a9cMdbDkWxotSmUeE26dnRlsnU,4537
15
16
  dayhoff_tools/cli/batch/commands/embed_t5.py,sha256=QXFydAw0wndevdzXF1cxikxMmvn1BuQ5p9lwutQFajU,11453
16
17
  dayhoff_tools/cli/batch/commands/finalize.py,sha256=REt-1f1Vb7OycJJ6JTUV_xJQVKBPXz0RxxhEuNfd39g,7957
17
- dayhoff_tools/cli/batch/commands/list_jobs.py,sha256=55KqOuhpxpuFBk95Dv2Wd79MprJh1ouy_SWDmBpT7Zg,2438
18
+ dayhoff_tools/cli/batch/commands/list_jobs.py,sha256=COfxZddDVUAHeTayNAB3ruYNhgrE3osgFxY2qzf33cg,4284
18
19
  dayhoff_tools/cli/batch/commands/local.py,sha256=dZeKhNakaM1jS-EoByAwg1nWspRRoOmYzcwzjEKBaIA,3226
19
20
  dayhoff_tools/cli/batch/commands/logs.py,sha256=ctgJksdzFmqBdD18ePPsZe2BpuJYtHz2xAaMPnUplmQ,5293
20
21
  dayhoff_tools/cli/batch/commands/retry.py,sha256=PbstKr7iP7Uu4csisp3cvM8al9YOFw27XVlc591jLbE,4509
21
- dayhoff_tools/cli/batch/commands/status.py,sha256=0xDIVQzgYl5mgpHcDhIwmy44IVt0B5L33LHX91nfWuU,7684
22
+ dayhoff_tools/cli/batch/commands/status.py,sha256=k8ltke62vKUX_4MmnFDEd6H2s8KmVFA71UUXLbleA7o,9790
22
23
  dayhoff_tools/cli/batch/commands/submit.py,sha256=AXbvSReN8fLlzR5swE81pH7yvbCC1GUMCbsDrfoHAws,6786
23
24
  dayhoff_tools/cli/batch/job_id.py,sha256=mqr8SwcPlUWIYLaR_C4kACmL2ZFK8jaddd7B-45-XaQ,4246
24
25
  dayhoff_tools/cli/batch/manifest.py,sha256=blUw2ASxFTmdLV5ikz4VD91n5CN0V37rlyX11M0H2VE,8624
@@ -70,7 +71,7 @@ dayhoff_tools/intake/uniprot.py,sha256=BZYJQF63OtPcBBnQ7_P9gulxzJtqyorgyuDiPeOJq
70
71
  dayhoff_tools/logs.py,sha256=DKdeP0k0kliRcilwvX0mUB2eipO5BdWUeHwh-VnsICs,838
71
72
  dayhoff_tools/sqlite.py,sha256=jV55ikF8VpTfeQqqlHSbY8OgfyfHj8zgHNpZjBLos_E,18672
72
73
  dayhoff_tools/warehouse.py,sha256=UETBtZD3r7WgvURqfGbyHlT7cxoiVq8isjzMuerKw8I,24475
73
- dayhoff_tools-1.14.7.dist-info/METADATA,sha256=k1Nua41haYs1TSTYGO-tgESgBcVf2hAhFAnI2WJ9Yck,3184
74
- dayhoff_tools-1.14.7.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
75
- dayhoff_tools-1.14.7.dist-info/entry_points.txt,sha256=iAf4jteNqW3cJm6CO6czLxjW3vxYKsyGLZ8WGmxamSc,49
76
- dayhoff_tools-1.14.7.dist-info/RECORD,,
74
+ dayhoff_tools-1.14.8.dist-info/METADATA,sha256=86xFQ0fq394JPcdZ-o2lRsWuDoLiGDkSw5SsePljptM,3184
75
+ dayhoff_tools-1.14.8.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
76
+ dayhoff_tools-1.14.8.dist-info/entry_points.txt,sha256=iAf4jteNqW3cJm6CO6czLxjW3vxYKsyGLZ8WGmxamSc,49
77
+ dayhoff_tools-1.14.8.dist-info/RECORD,,