amina-cli 0.2.5__tar.gz → 0.2.7__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.
- {amina_cli-0.2.5 → amina_cli-0.2.7}/PKG-INFO +1 -1
- {amina_cli-0.2.5 → amina_cli-0.2.7}/pyproject.toml +1 -1
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/__init__.py +1 -1
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/auth.py +31 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/client.py +84 -4
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/jobs_cmd.py +144 -27
- amina_cli-0.2.7/src/amina_cli/commands/tools/analysis/residue_accessibility.py +121 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/registry.py +11 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/.gitignore +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/LICENSE +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/README.md +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/__init__.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/auth_cmd.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/run_cmd.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/__init__.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/analysis/__init__.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/analysis/hydrophobicity.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/analysis/mmseqs2_cluster.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/analysis/rmsd.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/analysis/sasa.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/analysis/simple_rmsd.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/analysis/surface_charge.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/analysis/usalign.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/design/__init__.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/design/esm_if1.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/design/protein_mc.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/design/proteinmpnn.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/design/rfdiffusion.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/display.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/folding/__init__.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/folding/boltz2.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/folding/esmfold.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/folding/openfold3.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/folding/protenix.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/interactions/__init__.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/interactions/autodock_vina.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/interactions/diffdock.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/interactions/dockq.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/interactions/emngly.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/interactions/glycosylation_ensemble.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/interactions/interface_identifier.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/interactions/isoglyp.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/interactions/lmngly.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/interactions/p2rank.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/interactions/pesto.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/properties/__init__.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/properties/aminosol.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/properties/esm1v.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/properties/esm2_embedding.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/utilities/__init__.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/utilities/activesite_verifier.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/utilities/chain_select.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/utilities/distance_calculator.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/utilities/maxit_convert.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/utilities/mol_size_calculator.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/utilities/obabel_convert.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/utilities/pdb_bfactor_overwrite.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/utilities/pdb_cleaner.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/utilities/pdb_quality_assessment.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/utilities/pdb_to_fasta.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/utilities/protein_relaxer.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools_cmd.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/main.py +0 -0
- {amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/storage.py +0 -0
|
@@ -336,3 +336,34 @@ def update_job_status(job_id: str, status: str) -> None:
|
|
|
336
336
|
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
|
|
337
337
|
with os.fdopen(os.open(JOBS_FILE, flags, 0o600), "w") as f:
|
|
338
338
|
f.write(content)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def update_job_info(job_id: str, updates: dict) -> None:
|
|
342
|
+
"""
|
|
343
|
+
Merge fields into a saved job entry.
|
|
344
|
+
|
|
345
|
+
Used to persist a resolved call_id so subsequent polls skip the queue check.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
job_id: Job ID to update
|
|
349
|
+
updates: Dict of fields to merge (e.g., {"call_id": "fc-xxx"})
|
|
350
|
+
"""
|
|
351
|
+
import os
|
|
352
|
+
|
|
353
|
+
if not JOBS_FILE.exists():
|
|
354
|
+
return
|
|
355
|
+
|
|
356
|
+
try:
|
|
357
|
+
jobs = json.loads(JOBS_FILE.read_text())
|
|
358
|
+
except (json.JSONDecodeError, IOError):
|
|
359
|
+
return
|
|
360
|
+
|
|
361
|
+
for job in jobs:
|
|
362
|
+
if job.get("job_id") == job_id:
|
|
363
|
+
job.update(updates)
|
|
364
|
+
break
|
|
365
|
+
|
|
366
|
+
content = json.dumps(jobs, indent=2)
|
|
367
|
+
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
|
|
368
|
+
with os.fdopen(os.open(JOBS_FILE, flags, 0o600), "w") as f:
|
|
369
|
+
f.write(content)
|
|
@@ -20,7 +20,13 @@ from typing import Any, Optional
|
|
|
20
20
|
from uuid import uuid4
|
|
21
21
|
|
|
22
22
|
from amina_cli.auth import get_api_key, AuthError
|
|
23
|
-
from amina_cli.registry import
|
|
23
|
+
from amina_cli.registry import (
|
|
24
|
+
get_submit_endpoint,
|
|
25
|
+
get_status_endpoint,
|
|
26
|
+
get_queued_status_endpoint,
|
|
27
|
+
get_cancel_endpoint,
|
|
28
|
+
ToolNotFoundError,
|
|
29
|
+
)
|
|
24
30
|
|
|
25
31
|
|
|
26
32
|
# Default polling configuration
|
|
@@ -92,10 +98,12 @@ class InsufficientCreditsError(ClientError):
|
|
|
92
98
|
class JobLimitError(ClientError):
|
|
93
99
|
"""Raised when user has reached job concurrency limits."""
|
|
94
100
|
|
|
95
|
-
def __init__(self, limit_type: str, current: int, limit: int, message: str):
|
|
101
|
+
def __init__(self, limit_type: str, current: int, limit: int, message: str, **kwargs: Any):
|
|
96
102
|
self.limit_type = limit_type
|
|
97
103
|
self.current = current
|
|
98
104
|
self.limit = limit
|
|
105
|
+
self.running = kwargs.get("running", 0)
|
|
106
|
+
self.queued = kwargs.get("queued", 0)
|
|
99
107
|
super().__init__(message)
|
|
100
108
|
|
|
101
109
|
|
|
@@ -185,6 +193,7 @@ async def _poll_queued_until_submitted(
|
|
|
185
193
|
poll_interval: float = 5.0, # Longer interval for queue polling
|
|
186
194
|
max_poll_interval: float = 30.0,
|
|
187
195
|
on_position_update: Optional[QueuePositionCallback] = None,
|
|
196
|
+
headers: Optional[dict[str, str]] = None,
|
|
188
197
|
) -> tuple[str, str]:
|
|
189
198
|
"""
|
|
190
199
|
Poll queued_job_status until job is submitted.
|
|
@@ -229,7 +238,7 @@ async def _poll_queued_until_submitted(
|
|
|
229
238
|
status_url = f"{queued_endpoint}?job_id={job_id}"
|
|
230
239
|
|
|
231
240
|
try:
|
|
232
|
-
response = await client.get(status_url)
|
|
241
|
+
response = await client.get(status_url, headers=headers or {})
|
|
233
242
|
except httpx.RequestError as e:
|
|
234
243
|
# Log transient error and continue polling
|
|
235
244
|
print(f"Queue poll network error (will retry): {type(e).__name__}: {e}")
|
|
@@ -396,6 +405,8 @@ async def run_tool(
|
|
|
396
405
|
current=detail.get("current", 0),
|
|
397
406
|
limit=detail.get("limit", 0),
|
|
398
407
|
message=detail.get("message", "Job limit reached. Please wait for running jobs to complete."),
|
|
408
|
+
running=detail.get("running", 0),
|
|
409
|
+
queued=detail.get("queued", 0),
|
|
399
410
|
)
|
|
400
411
|
except (ValueError, KeyError):
|
|
401
412
|
raise JobLimitError(
|
|
@@ -438,6 +449,7 @@ async def run_tool(
|
|
|
438
449
|
poll_interval=5.0, # Slower for queue
|
|
439
450
|
max_poll_interval=30.0,
|
|
440
451
|
on_position_update=on_queue_position_update,
|
|
452
|
+
headers=auth_headers,
|
|
441
453
|
)
|
|
442
454
|
else:
|
|
443
455
|
# Job was submitted immediately
|
|
@@ -663,6 +675,8 @@ async def submit_tool(
|
|
|
663
675
|
current=detail.get("current", 0),
|
|
664
676
|
limit=detail.get("limit", 0),
|
|
665
677
|
message=detail.get("message", "Job limit reached. Please wait for running jobs to complete."),
|
|
678
|
+
running=detail.get("running", 0),
|
|
679
|
+
queued=detail.get("queued", 0),
|
|
666
680
|
)
|
|
667
681
|
except (ValueError, KeyError):
|
|
668
682
|
raise JobLimitError(
|
|
@@ -855,12 +869,16 @@ async def check_queued_job_status(job_id: str) -> dict[str, Any]:
|
|
|
855
869
|
- {"status": "submitted", "call_id": "..."} if spawned
|
|
856
870
|
- {"status": "failed", "error": "..."} if failed/expired
|
|
857
871
|
"""
|
|
872
|
+
from amina_cli.auth import get_api_key
|
|
873
|
+
|
|
858
874
|
queued_endpoint = get_queued_status_endpoint()
|
|
859
875
|
status_url = f"{queued_endpoint}?job_id={job_id}"
|
|
876
|
+
api_key = get_api_key()
|
|
877
|
+
headers = {"Authorization": f"Bearer {api_key}"} if api_key else {}
|
|
860
878
|
|
|
861
879
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
862
880
|
try:
|
|
863
|
-
response = await client.get(status_url)
|
|
881
|
+
response = await client.get(status_url, headers=headers)
|
|
864
882
|
except httpx.RequestError as e:
|
|
865
883
|
raise ToolExecutionError(f"Network error checking queued job status: {str(e)}")
|
|
866
884
|
|
|
@@ -883,6 +901,68 @@ def check_queued_job_status_sync(job_id: str) -> dict[str, Any]:
|
|
|
883
901
|
return asyncio.run(check_queued_job_status(job_id))
|
|
884
902
|
|
|
885
903
|
|
|
904
|
+
async def cancel_job(job_id: str, api_key: Optional[str] = None) -> dict[str, Any]:
|
|
905
|
+
"""
|
|
906
|
+
Cancel a queued or running job.
|
|
907
|
+
|
|
908
|
+
Args:
|
|
909
|
+
job_id: Job ID to cancel
|
|
910
|
+
api_key: Optional API key (uses stored key if not provided)
|
|
911
|
+
|
|
912
|
+
Returns:
|
|
913
|
+
Dict with status info:
|
|
914
|
+
- {"status": "cancelled"} on success
|
|
915
|
+
- {"status": "not_found"} if job doesn't exist
|
|
916
|
+
- {"status": "already_finished"} if job already completed/failed/cancelled
|
|
917
|
+
|
|
918
|
+
Raises:
|
|
919
|
+
AuthenticationError: If API key is invalid
|
|
920
|
+
ToolExecutionError: If request fails unexpectedly
|
|
921
|
+
"""
|
|
922
|
+
if api_key is None:
|
|
923
|
+
try:
|
|
924
|
+
api_key = get_api_key()
|
|
925
|
+
except AuthError as e:
|
|
926
|
+
raise AuthenticationError(str(e))
|
|
927
|
+
|
|
928
|
+
cancel_endpoint = get_cancel_endpoint()
|
|
929
|
+
url = f"{cancel_endpoint}?job_id={job_id}"
|
|
930
|
+
|
|
931
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
932
|
+
try:
|
|
933
|
+
response = await client.post(url, headers={"Authorization": f"Bearer {api_key}"})
|
|
934
|
+
except httpx.RequestError as e:
|
|
935
|
+
raise ToolExecutionError(f"Network error cancelling job: {str(e)}")
|
|
936
|
+
|
|
937
|
+
if response.status_code == 401:
|
|
938
|
+
raise AuthenticationError("Invalid API key. Get a new one at: https://app.aminoanalytica.com/settings/api")
|
|
939
|
+
|
|
940
|
+
if response.status_code == 404:
|
|
941
|
+
return {"status": "not_found", "error": "Job not found"}
|
|
942
|
+
|
|
943
|
+
if response.status_code == 409:
|
|
944
|
+
return {"status": "already_finished", "error": "Job already finished"}
|
|
945
|
+
|
|
946
|
+
if response.status_code == 200:
|
|
947
|
+
return response.json()
|
|
948
|
+
|
|
949
|
+
raise ToolExecutionError(f"Cancel failed with HTTP {response.status_code}: {response.text}")
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
def cancel_job_sync(job_id: str, api_key: Optional[str] = None) -> dict[str, Any]:
|
|
953
|
+
"""
|
|
954
|
+
Synchronous wrapper for cancel_job.
|
|
955
|
+
|
|
956
|
+
Args:
|
|
957
|
+
job_id: Job ID to cancel
|
|
958
|
+
api_key: Optional API key
|
|
959
|
+
|
|
960
|
+
Returns:
|
|
961
|
+
Dict with status info
|
|
962
|
+
"""
|
|
963
|
+
return asyncio.run(cancel_job(job_id, api_key))
|
|
964
|
+
|
|
965
|
+
|
|
886
966
|
def check_endpoint_health(tool_name: str) -> bool:
|
|
887
967
|
"""
|
|
888
968
|
Check if a tool's endpoint is reachable.
|
|
@@ -21,6 +21,65 @@ app = typer.Typer(no_args_is_help=True)
|
|
|
21
21
|
console = Console()
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
def _resolve_job_status(job_info: dict) -> dict:
|
|
25
|
+
"""
|
|
26
|
+
Resolve the current status of a job, handling both spawned and queued jobs.
|
|
27
|
+
|
|
28
|
+
If call_id is present, polls Modal directly. If missing (queued job),
|
|
29
|
+
checks the queue first and resolves call_id when available.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
job_info: Full job info dict from local history
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Dict with status info (same shape as check_job_status_sync)
|
|
36
|
+
"""
|
|
37
|
+
from amina_cli.client import check_job_status_sync, check_queued_job_status_sync
|
|
38
|
+
from amina_cli.auth import update_job_info
|
|
39
|
+
|
|
40
|
+
call_id = job_info.get("call_id", "")
|
|
41
|
+
job_id = job_info.get("job_id", "")
|
|
42
|
+
|
|
43
|
+
if call_id:
|
|
44
|
+
# Normal path: job was spawned, poll Modal directly
|
|
45
|
+
return check_job_status_sync(
|
|
46
|
+
call_id=call_id,
|
|
47
|
+
job_id=job_id,
|
|
48
|
+
user_id=job_info.get("user_id", ""),
|
|
49
|
+
tool_name=job_info.get("tool_name", ""),
|
|
50
|
+
reserved_cost=job_info.get("reserved_cost", 0.0),
|
|
51
|
+
compute_type=job_info.get("compute_type", "cpu"),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Queued path: no call_id yet, check queue status
|
|
55
|
+
queue_result = check_queued_job_status_sync(job_id)
|
|
56
|
+
queue_status = queue_result.get("status", "")
|
|
57
|
+
|
|
58
|
+
if queue_status in ("queued", "not_found"):
|
|
59
|
+
return queue_result
|
|
60
|
+
|
|
61
|
+
if queue_status == "failed":
|
|
62
|
+
return queue_result
|
|
63
|
+
|
|
64
|
+
# Job has been spawned (submitted/running/completed) — try to get call_id
|
|
65
|
+
resolved_call_id = queue_result.get("call_id", "")
|
|
66
|
+
if not resolved_call_id:
|
|
67
|
+
# Race condition: status changed but call_id not yet written
|
|
68
|
+
return {"status": "queued", "message": "Job is being spawned"}
|
|
69
|
+
|
|
70
|
+
# Persist call_id so future polls skip the queue check
|
|
71
|
+
update_job_info(job_id, {"call_id": resolved_call_id})
|
|
72
|
+
|
|
73
|
+
return check_job_status_sync(
|
|
74
|
+
call_id=resolved_call_id,
|
|
75
|
+
job_id=job_id,
|
|
76
|
+
user_id=job_info.get("user_id", ""),
|
|
77
|
+
tool_name=job_info.get("tool_name", ""),
|
|
78
|
+
reserved_cost=job_info.get("reserved_cost", 0.0),
|
|
79
|
+
compute_type=job_info.get("compute_type", "cpu"),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
24
83
|
@app.command("list")
|
|
25
84
|
def list_jobs(
|
|
26
85
|
limit: int = typer.Option(
|
|
@@ -101,7 +160,7 @@ def status(
|
|
|
101
160
|
amina jobs status abc123 --json
|
|
102
161
|
"""
|
|
103
162
|
from amina_cli.auth import get_job_info
|
|
104
|
-
from amina_cli.client import
|
|
163
|
+
from amina_cli.client import ToolExecutionError
|
|
105
164
|
|
|
106
165
|
results = []
|
|
107
166
|
|
|
@@ -121,14 +180,7 @@ def status(
|
|
|
121
180
|
|
|
122
181
|
# Query server for current status
|
|
123
182
|
try:
|
|
124
|
-
status_result =
|
|
125
|
-
call_id=job_info.get("call_id", ""),
|
|
126
|
-
job_id=job_info.get("job_id", job_id),
|
|
127
|
-
user_id=job_info.get("user_id", ""),
|
|
128
|
-
tool_name=job_info.get("tool_name", ""),
|
|
129
|
-
reserved_cost=job_info.get("reserved_cost", 0.0),
|
|
130
|
-
compute_type=job_info.get("compute_type", "cpu"),
|
|
131
|
-
)
|
|
183
|
+
status_result = _resolve_job_status(job_info)
|
|
132
184
|
results.append(
|
|
133
185
|
{
|
|
134
186
|
"job_id": job_info.get("job_id", job_id),
|
|
@@ -212,7 +264,7 @@ def wait(
|
|
|
212
264
|
amina jobs wait abc123 --timeout 7200 --poll-interval 30
|
|
213
265
|
"""
|
|
214
266
|
from amina_cli.auth import get_job_info
|
|
215
|
-
from amina_cli.client import
|
|
267
|
+
from amina_cli.client import ToolExecutionError
|
|
216
268
|
|
|
217
269
|
start_time = time.time()
|
|
218
270
|
pending_jobs = set(job_ids)
|
|
@@ -237,14 +289,7 @@ def wait(
|
|
|
237
289
|
continue
|
|
238
290
|
|
|
239
291
|
try:
|
|
240
|
-
status_result =
|
|
241
|
-
call_id=job_info.get("call_id", ""),
|
|
242
|
-
job_id=job_info.get("job_id", job_id),
|
|
243
|
-
user_id=job_info.get("user_id", ""),
|
|
244
|
-
tool_name=job_info.get("tool_name", ""),
|
|
245
|
-
reserved_cost=job_info.get("reserved_cost", 0.0),
|
|
246
|
-
compute_type=job_info.get("compute_type", "cpu"),
|
|
247
|
-
)
|
|
292
|
+
status_result = _resolve_job_status(job_info)
|
|
248
293
|
|
|
249
294
|
if status_result.get("status") in ("completed", "failed"):
|
|
250
295
|
results[job_id] = {
|
|
@@ -290,6 +335,80 @@ def wait(
|
|
|
290
335
|
raise typer.Exit(1)
|
|
291
336
|
|
|
292
337
|
|
|
338
|
+
@app.command("cancel")
|
|
339
|
+
def cancel(
|
|
340
|
+
job_ids: list[str] = typer.Argument(
|
|
341
|
+
...,
|
|
342
|
+
help="Job ID(s) to cancel",
|
|
343
|
+
),
|
|
344
|
+
json_output: bool = typer.Option(
|
|
345
|
+
False,
|
|
346
|
+
"--json",
|
|
347
|
+
help="Output as JSON",
|
|
348
|
+
),
|
|
349
|
+
):
|
|
350
|
+
"""
|
|
351
|
+
Cancel one or more queued or running jobs.
|
|
352
|
+
|
|
353
|
+
Sends a cancellation request to the server. Credits are fully refunded.
|
|
354
|
+
|
|
355
|
+
Examples:
|
|
356
|
+
amina jobs cancel abc123
|
|
357
|
+
amina jobs cancel abc123 def456
|
|
358
|
+
amina jobs cancel abc123 --json
|
|
359
|
+
"""
|
|
360
|
+
from amina_cli.auth import get_job_info, update_job_status
|
|
361
|
+
from amina_cli.client import cancel_job_sync, ToolExecutionError
|
|
362
|
+
|
|
363
|
+
results = []
|
|
364
|
+
|
|
365
|
+
for job_id in job_ids:
|
|
366
|
+
job_info = get_job_info(job_id)
|
|
367
|
+
|
|
368
|
+
if not job_info:
|
|
369
|
+
results.append({"job_id": job_id, "status": "not_found", "error": "Job not found in local history"})
|
|
370
|
+
continue
|
|
371
|
+
|
|
372
|
+
full_job_id = job_info.get("job_id", job_id)
|
|
373
|
+
|
|
374
|
+
try:
|
|
375
|
+
cancel_result = cancel_job_sync(full_job_id)
|
|
376
|
+
status = cancel_result.get("status", "error")
|
|
377
|
+
|
|
378
|
+
if status == "cancelled":
|
|
379
|
+
update_job_status(full_job_id, "cancelled")
|
|
380
|
+
|
|
381
|
+
results.append(
|
|
382
|
+
{
|
|
383
|
+
"job_id": full_job_id,
|
|
384
|
+
"tool_name": job_info.get("tool_name", ""),
|
|
385
|
+
**cancel_result,
|
|
386
|
+
}
|
|
387
|
+
)
|
|
388
|
+
except ToolExecutionError as e:
|
|
389
|
+
results.append({"job_id": full_job_id, "status": "error", "error": str(e)})
|
|
390
|
+
|
|
391
|
+
if json_output:
|
|
392
|
+
console.print(json.dumps(results, indent=2, default=str))
|
|
393
|
+
return
|
|
394
|
+
|
|
395
|
+
for result in results:
|
|
396
|
+
job_id_short = result.get("job_id", "")[:8]
|
|
397
|
+
status_str = result.get("status", "error")
|
|
398
|
+
tool_name = result.get("tool_name", "")
|
|
399
|
+
label = f" ({tool_name})" if tool_name else ""
|
|
400
|
+
|
|
401
|
+
if status_str == "cancelled":
|
|
402
|
+
console.print(f"[green]\u2713[/green] {job_id_short}...{label}: [green]cancelled[/green]")
|
|
403
|
+
elif status_str == "already_finished":
|
|
404
|
+
console.print(f"[yellow]\u2717[/yellow] {job_id_short}...{label}: [yellow]already finished[/yellow]")
|
|
405
|
+
elif status_str == "not_found":
|
|
406
|
+
console.print(f"[dim]?[/dim] {job_id_short}...: [dim]not found[/dim]")
|
|
407
|
+
else:
|
|
408
|
+
error = result.get("error", "")
|
|
409
|
+
console.print(f"[red]\u2717[/red] {job_id_short}...{label}: [red]{status_str}[/red] - {error}")
|
|
410
|
+
|
|
411
|
+
|
|
293
412
|
@app.command("download")
|
|
294
413
|
def download(
|
|
295
414
|
job_id: str = typer.Argument(
|
|
@@ -312,7 +431,7 @@ def download(
|
|
|
312
431
|
amina jobs download abc123 -o ./results/
|
|
313
432
|
"""
|
|
314
433
|
from amina_cli.auth import get_job_info
|
|
315
|
-
from amina_cli.client import
|
|
434
|
+
from amina_cli.client import ToolExecutionError
|
|
316
435
|
from amina_cli.storage import download_results, StorageError
|
|
317
436
|
|
|
318
437
|
# Look up job info
|
|
@@ -324,18 +443,16 @@ def download(
|
|
|
324
443
|
|
|
325
444
|
# Check status
|
|
326
445
|
try:
|
|
327
|
-
status_result =
|
|
328
|
-
call_id=job_info.get("call_id", ""),
|
|
329
|
-
job_id=job_info.get("job_id", job_id),
|
|
330
|
-
user_id=job_info.get("user_id", ""),
|
|
331
|
-
tool_name=job_info.get("tool_name", ""),
|
|
332
|
-
reserved_cost=job_info.get("reserved_cost", 0.0),
|
|
333
|
-
compute_type=job_info.get("compute_type", "cpu"),
|
|
334
|
-
)
|
|
446
|
+
status_result = _resolve_job_status(job_info)
|
|
335
447
|
except ToolExecutionError as e:
|
|
336
448
|
console.print(f"[red]Error checking job status:[/red] {e}")
|
|
337
449
|
raise typer.Exit(1)
|
|
338
450
|
|
|
451
|
+
if status_result.get("status") == "queued":
|
|
452
|
+
console.print("[yellow]Job is still queued.[/yellow] Wait for completion first:")
|
|
453
|
+
console.print(f" amina jobs wait {job_id}")
|
|
454
|
+
raise typer.Exit(1)
|
|
455
|
+
|
|
339
456
|
if status_result.get("status") == "running":
|
|
340
457
|
console.print("[yellow]Job is still running.[/yellow] Wait for completion first:")
|
|
341
458
|
console.print(f" amina jobs wait {job_id}")
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Residue Accessibility analysis tool for the Amina CLI."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
METADATA = {
|
|
9
|
+
"name": "residue_accessibility",
|
|
10
|
+
"display_name": "Residue Accessibility",
|
|
11
|
+
"category": "analysis",
|
|
12
|
+
"description": "Score residues for binder accessibility beyond SASA using depth, visibility, and curvature",
|
|
13
|
+
"modal_function_name": "residue_accessibility_worker",
|
|
14
|
+
"modal_app_name": "residue-accessibility-api",
|
|
15
|
+
"status": "available",
|
|
16
|
+
"outputs": {
|
|
17
|
+
"json_filepath": "Full structured results (residues, patches, recommendation)",
|
|
18
|
+
"csv_filepath": "Per-residue accessibility scores in CSV format",
|
|
19
|
+
"pdb_filepath": "PDB with B-factors set to accessibility tier scores",
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
console = Console()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def register(app: typer.Typer):
|
|
27
|
+
"""Register this tool's command with the app."""
|
|
28
|
+
from amina_cli.commands.tools import run_tool_with_progress
|
|
29
|
+
|
|
30
|
+
@app.command("residue-accessibility")
|
|
31
|
+
def run_residue_accessibility(
|
|
32
|
+
pdb: Path = typer.Option(
|
|
33
|
+
...,
|
|
34
|
+
"--pdb",
|
|
35
|
+
"-p",
|
|
36
|
+
help="Path to PDB file containing protein structure",
|
|
37
|
+
exists=True,
|
|
38
|
+
),
|
|
39
|
+
output: Optional[Path] = typer.Option(
|
|
40
|
+
None,
|
|
41
|
+
"--output",
|
|
42
|
+
"-o",
|
|
43
|
+
help="Output directory for results (required unless --background)",
|
|
44
|
+
),
|
|
45
|
+
residues: Optional[str] = typer.Option(
|
|
46
|
+
None,
|
|
47
|
+
"--residues",
|
|
48
|
+
"-r",
|
|
49
|
+
help="Residues to score in CHAIN:RESNUM format (e.g., 'A:42,A:43,B:10'). "
|
|
50
|
+
"If omitted, all surface-exposed residues are scored.",
|
|
51
|
+
),
|
|
52
|
+
rsa_threshold: float = typer.Option(
|
|
53
|
+
0.20,
|
|
54
|
+
"--rsa-threshold",
|
|
55
|
+
help="RSA cutoff for the exposure gate (0.0-1.0). Residues below this are excluded.",
|
|
56
|
+
),
|
|
57
|
+
n_rays: int = typer.Option(
|
|
58
|
+
200,
|
|
59
|
+
"--n-rays",
|
|
60
|
+
help="Number of hemisphere rays per residue for visibility scoring (10-1000).",
|
|
61
|
+
),
|
|
62
|
+
job_name: Optional[str] = typer.Option(
|
|
63
|
+
None,
|
|
64
|
+
"--job-name",
|
|
65
|
+
"-j",
|
|
66
|
+
help="Custom job name for output files (default: random 4-letter code)",
|
|
67
|
+
),
|
|
68
|
+
background: bool = typer.Option(
|
|
69
|
+
False,
|
|
70
|
+
"--background",
|
|
71
|
+
"-b",
|
|
72
|
+
help="Submit job and return immediately without waiting for completion",
|
|
73
|
+
),
|
|
74
|
+
):
|
|
75
|
+
"""
|
|
76
|
+
Score protein residues for binder accessibility.
|
|
77
|
+
|
|
78
|
+
Goes beyond SASA to measure residue depth, outward visibility (ray casting),
|
|
79
|
+
and surface curvature. Returns a ranked list of residues scored 0–1 for
|
|
80
|
+
binder accessibility.
|
|
81
|
+
|
|
82
|
+
Examples:
|
|
83
|
+
amina run residue-accessibility --pdb ./target.pdb -o ./results/
|
|
84
|
+
amina run residue-accessibility --pdb ./target.pdb -r "A:42,A:43,A:45" -o ./results/
|
|
85
|
+
amina run residue-accessibility --pdb ./target.pdb --rsa-threshold 0.15 -o ./results/
|
|
86
|
+
amina run residue-accessibility --pdb ./target.pdb -j myjob -o ./results/ --background
|
|
87
|
+
"""
|
|
88
|
+
# Validate required options
|
|
89
|
+
if output is None and not background:
|
|
90
|
+
console.print("[red]Error:[/red] --output / -o is required (unless using --background)")
|
|
91
|
+
raise typer.Exit(1)
|
|
92
|
+
|
|
93
|
+
# Validate residue format if provided
|
|
94
|
+
if residues:
|
|
95
|
+
for token in residues.split(","):
|
|
96
|
+
token = token.strip()
|
|
97
|
+
if ":" not in token:
|
|
98
|
+
console.print(
|
|
99
|
+
f"[red]Error:[/red] Invalid residue format '{token}'. Expected CHAIN:RESNUM (e.g., 'A:42')."
|
|
100
|
+
)
|
|
101
|
+
raise typer.Exit(1)
|
|
102
|
+
|
|
103
|
+
# Read PDB file content
|
|
104
|
+
pdb_content = pdb.read_text()
|
|
105
|
+
console.print(f"Read PDB file: {pdb.name}")
|
|
106
|
+
|
|
107
|
+
# Build params dict matching worker's expected fields
|
|
108
|
+
params = {
|
|
109
|
+
"pdb_content": pdb_content,
|
|
110
|
+
"rsa_threshold": rsa_threshold,
|
|
111
|
+
"n_rays": n_rays,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if residues:
|
|
115
|
+
params["residues"] = residues
|
|
116
|
+
|
|
117
|
+
if job_name:
|
|
118
|
+
params["job_name"] = job_name
|
|
119
|
+
|
|
120
|
+
# Execute
|
|
121
|
+
run_tool_with_progress("residue_accessibility", params, output, background=background)
|
|
@@ -80,6 +80,17 @@ def get_queued_status_endpoint() -> str:
|
|
|
80
80
|
return f"{base}-queued-job-status.modal.run"
|
|
81
81
|
|
|
82
82
|
|
|
83
|
+
def get_cancel_endpoint() -> str:
|
|
84
|
+
"""
|
|
85
|
+
Get the cancel_job endpoint URL.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Cancel job endpoint URL (job_id added as query param by client)
|
|
89
|
+
"""
|
|
90
|
+
base = _get_gateway_base()
|
|
91
|
+
return f"{base}-cancel-job.modal.run"
|
|
92
|
+
|
|
93
|
+
|
|
83
94
|
# Legacy function for backwards compatibility
|
|
84
95
|
def get_tool_endpoint(name: str) -> str:
|
|
85
96
|
"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/analysis/mmseqs2_cluster.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/interactions/autodock_vina.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/properties/esm2_embedding.py
RENAMED
|
File without changes
|
|
File without changes
|
{amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/utilities/activesite_verifier.py
RENAMED
|
File without changes
|
|
File without changes
|
{amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/utilities/distance_calculator.py
RENAMED
|
File without changes
|
|
File without changes
|
{amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/utilities/mol_size_calculator.py
RENAMED
|
File without changes
|
{amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/utilities/obabel_convert.py
RENAMED
|
File without changes
|
{amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/utilities/pdb_bfactor_overwrite.py
RENAMED
|
File without changes
|
|
File without changes
|
{amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/utilities/pdb_quality_assessment.py
RENAMED
|
File without changes
|
|
File without changes
|
{amina_cli-0.2.5 → amina_cli-0.2.7}/src/amina_cli/commands/tools/utilities/protein_relaxer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|