amina-cli 0.1.0__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.
Files changed (62) hide show
  1. amina_cli/__init__.py +12 -0
  2. amina_cli/auth.py +338 -0
  3. amina_cli/client.py +847 -0
  4. amina_cli/commands/__init__.py +9 -0
  5. amina_cli/commands/auth_cmd.py +181 -0
  6. amina_cli/commands/jobs_cmd.py +351 -0
  7. amina_cli/commands/run_cmd.py +46 -0
  8. amina_cli/commands/tools/__init__.py +382 -0
  9. amina_cli/commands/tools/analysis/__init__.py +1 -0
  10. amina_cli/commands/tools/analysis/hydrophobicity.py +105 -0
  11. amina_cli/commands/tools/analysis/mmseqs2_cluster.py +149 -0
  12. amina_cli/commands/tools/analysis/rmsd.py +188 -0
  13. amina_cli/commands/tools/analysis/sasa.py +86 -0
  14. amina_cli/commands/tools/analysis/simple_rmsd.py +131 -0
  15. amina_cli/commands/tools/analysis/surface_charge.py +171 -0
  16. amina_cli/commands/tools/analysis/usalign.py +158 -0
  17. amina_cli/commands/tools/design/__init__.py +1 -0
  18. amina_cli/commands/tools/design/esm_if1.py +166 -0
  19. amina_cli/commands/tools/design/protein_mc.py +145 -0
  20. amina_cli/commands/tools/design/proteinmpnn.py +137 -0
  21. amina_cli/commands/tools/design/rfdiffusion.py +306 -0
  22. amina_cli/commands/tools/display.py +257 -0
  23. amina_cli/commands/tools/folding/__init__.py +1 -0
  24. amina_cli/commands/tools/folding/boltz2.py +382 -0
  25. amina_cli/commands/tools/folding/esmfold.py +105 -0
  26. amina_cli/commands/tools/folding/openfold3.py +400 -0
  27. amina_cli/commands/tools/folding/protenix.py +421 -0
  28. amina_cli/commands/tools/interactions/__init__.py +1 -0
  29. amina_cli/commands/tools/interactions/autodock_vina.py +194 -0
  30. amina_cli/commands/tools/interactions/diffdock.py +166 -0
  31. amina_cli/commands/tools/interactions/dockq.py +174 -0
  32. amina_cli/commands/tools/interactions/emngly.py +86 -0
  33. amina_cli/commands/tools/interactions/glycosylation_ensemble.py +146 -0
  34. amina_cli/commands/tools/interactions/interface_identifier.py +99 -0
  35. amina_cli/commands/tools/interactions/isoglyp.py +132 -0
  36. amina_cli/commands/tools/interactions/lmngly.py +108 -0
  37. amina_cli/commands/tools/interactions/p2rank.py +81 -0
  38. amina_cli/commands/tools/interactions/pesto.py +137 -0
  39. amina_cli/commands/tools/properties/__init__.py +1 -0
  40. amina_cli/commands/tools/properties/aminosol.py +152 -0
  41. amina_cli/commands/tools/properties/esm2_embedding.py +157 -0
  42. amina_cli/commands/tools/utilities/__init__.py +1 -0
  43. amina_cli/commands/tools/utilities/activesite_verifier.py +127 -0
  44. amina_cli/commands/tools/utilities/chain_select.py +167 -0
  45. amina_cli/commands/tools/utilities/distance_calculator.py +183 -0
  46. amina_cli/commands/tools/utilities/maxit_convert.py +101 -0
  47. amina_cli/commands/tools/utilities/mol_size_calculator.py +77 -0
  48. amina_cli/commands/tools/utilities/obabel_convert.py +123 -0
  49. amina_cli/commands/tools/utilities/pdb_bfactor_overwrite.py +130 -0
  50. amina_cli/commands/tools/utilities/pdb_cleaner.py +96 -0
  51. amina_cli/commands/tools/utilities/pdb_quality_assessment.py +79 -0
  52. amina_cli/commands/tools/utilities/pdb_to_fasta.py +76 -0
  53. amina_cli/commands/tools/utilities/protein_relaxer.py +123 -0
  54. amina_cli/commands/tools_cmd.py +284 -0
  55. amina_cli/main.py +72 -0
  56. amina_cli/registry.py +184 -0
  57. amina_cli/storage.py +295 -0
  58. amina_cli-0.1.0.dist-info/METADATA +136 -0
  59. amina_cli-0.1.0.dist-info/RECORD +62 -0
  60. amina_cli-0.1.0.dist-info/WHEEL +4 -0
  61. amina_cli-0.1.0.dist-info/entry_points.txt +2 -0
  62. amina_cli-0.1.0.dist-info/licenses/LICENSE +190 -0
amina_cli/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ """
2
+ Amina CLI - Command-line interface for AminoAnalytica protein engineering platform.
3
+
4
+ Installation:
5
+ pip install amina-cli
6
+
7
+ Quick start:
8
+ amina auth set-key "ami_your_api_key"
9
+ amina run esmfold --sequence "MKFLILLFNILCLFPVLAADNH"
10
+ """
11
+
12
+ __version__ = "0.1.0"
amina_cli/auth.py ADDED
@@ -0,0 +1,338 @@
1
+ """
2
+ Local credential storage for Amina CLI.
3
+
4
+ This module handles storing and retrieving API keys locally.
5
+ Validation happens server-side when the key is used.
6
+
7
+ Credentials are stored in ~/.amina/credentials.json
8
+
9
+ This is the same pattern used by:
10
+ - OpenAI CLI: stores key in ~/.openai
11
+ - AWS CLI: stores in ~/.aws/credentials
12
+ - Stripe CLI: stores in ~/.config/stripe
13
+ """
14
+
15
+ import json
16
+ from pathlib import Path
17
+ from typing import Optional
18
+
19
+ # Configuration directory and files
20
+ CONFIG_DIR = Path.home() / ".amina"
21
+ CREDENTIALS_FILE = CONFIG_DIR / "credentials.json"
22
+
23
+ # API key format validation
24
+ API_KEY_PREFIX = "ami_"
25
+ API_KEY_MIN_LENGTH = 36 # ami_ + 32 hex chars
26
+
27
+
28
+ class AuthError(Exception):
29
+ """Raised when authentication fails or credentials are missing."""
30
+
31
+ pass
32
+
33
+
34
+ def validate_key_format(key: str) -> bool:
35
+ """
36
+ Validate that a key has the correct format.
37
+
38
+ Only checks format, NOT validity. Server validates when key is used.
39
+
40
+ Args:
41
+ key: The API key to validate
42
+
43
+ Returns:
44
+ True if format is valid
45
+ """
46
+ if not key:
47
+ return False
48
+ if not key.startswith(API_KEY_PREFIX):
49
+ return False
50
+ if len(key) < API_KEY_MIN_LENGTH:
51
+ return False
52
+ return True
53
+
54
+
55
+ def set_api_key(key: str) -> None:
56
+ """
57
+ Store an API key locally.
58
+
59
+ Format is validated but the key is NOT verified against the server.
60
+ Verification happens when the key is used for the first time.
61
+
62
+ Args:
63
+ key: The API key to store
64
+
65
+ Raises:
66
+ ValueError: If key format is invalid
67
+ """
68
+ if not validate_key_format(key):
69
+ raise ValueError(
70
+ f"Invalid API key format. Must start with '{API_KEY_PREFIX}' "
71
+ f"and be at least {API_KEY_MIN_LENGTH} characters."
72
+ )
73
+
74
+ save_credentials({"api_key": key})
75
+
76
+
77
+ def save_credentials(data: dict) -> None:
78
+ """
79
+ Save credentials to the config file.
80
+
81
+ Follows the same security pattern as OpenAI CLI:
82
+ - Directory: 0o700 (owner only)
83
+ - File: 0o600 (owner read/write only)
84
+
85
+ Args:
86
+ data: Dictionary of credentials to save
87
+ """
88
+ import os
89
+
90
+ # Create config directory with restrictive permissions (owner only)
91
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True, mode=0o700)
92
+
93
+ # Write credentials file atomically with secure permissions
94
+ # This prevents a race condition where the file briefly exists with wrong permissions
95
+ content = json.dumps(data, indent=2)
96
+ flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
97
+ with os.fdopen(os.open(CREDENTIALS_FILE, flags, 0o600), "w") as f:
98
+ f.write(content)
99
+
100
+
101
+ def get_credentials() -> dict:
102
+ """
103
+ Load stored credentials.
104
+
105
+ Returns:
106
+ Dictionary with 'api_key' and any other stored data
107
+
108
+ Raises:
109
+ AuthError: If not authenticated
110
+ """
111
+ if not CREDENTIALS_FILE.exists():
112
+ raise AuthError(
113
+ "Not authenticated. Run: amina auth set-key <your_api_key>\n"
114
+ "Get an API key at: https://app.aminoanalytica.com/settings/api"
115
+ )
116
+
117
+ try:
118
+ data = json.loads(CREDENTIALS_FILE.read_text())
119
+ if not data.get("api_key"):
120
+ raise AuthError("Credentials file exists but no API key found.")
121
+ return data
122
+ except json.JSONDecodeError:
123
+ raise AuthError("Credentials file is corrupted. Run: amina auth set-key <key>")
124
+
125
+
126
+ def get_api_key() -> str:
127
+ """
128
+ Get the stored API key.
129
+
130
+ Convenience wrapper around get_credentials().
131
+
132
+ Returns:
133
+ The stored API key
134
+
135
+ Raises:
136
+ AuthError: If not authenticated
137
+ """
138
+ return get_credentials()["api_key"]
139
+
140
+
141
+ def clear_credentials() -> None:
142
+ """
143
+ Remove all stored credentials (logout).
144
+
145
+ Idempotent - safe to call even if not authenticated.
146
+ """
147
+ if CREDENTIALS_FILE.exists():
148
+ CREDENTIALS_FILE.unlink()
149
+
150
+
151
+ def get_key_display(key: str) -> str:
152
+ """
153
+ Get a safe display version of an API key.
154
+
155
+ Shows prefix and first few chars, hides the rest.
156
+
157
+ Args:
158
+ key: The full API key
159
+
160
+ Returns:
161
+ Safe display string like "ami_7f3k..."
162
+ """
163
+ if len(key) > 12:
164
+ return f"{key[:12]}..."
165
+ return key
166
+
167
+
168
+ def is_authenticated() -> bool:
169
+ """
170
+ Check if credentials are stored.
171
+
172
+ Returns:
173
+ True if credentials file exists with an API key
174
+ """
175
+ try:
176
+ get_credentials()
177
+ return True
178
+ except AuthError:
179
+ return False
180
+
181
+
182
+ def get_session_id() -> Optional[str]:
183
+ """
184
+ Get the linked session/conversation ID if set.
185
+
186
+ When a session ID is set, CLI outputs will be linked to that
187
+ web conversation for visibility in the dashboard.
188
+
189
+ Returns:
190
+ Session ID or None if not linked
191
+ """
192
+ try:
193
+ data = get_credentials()
194
+ return data.get("session_id")
195
+ except AuthError:
196
+ return None
197
+
198
+
199
+ def set_session_id(session_id: Optional[str]) -> None:
200
+ """
201
+ Link CLI outputs to a web conversation.
202
+
203
+ Args:
204
+ session_id: The conversation ID to link, or None to unlink
205
+ """
206
+ try:
207
+ data = get_credentials()
208
+ except AuthError:
209
+ raise AuthError("Must be authenticated before linking a session.")
210
+
211
+ if session_id:
212
+ data["session_id"] = session_id
213
+ else:
214
+ data.pop("session_id", None)
215
+
216
+ save_credentials(data)
217
+
218
+
219
+ # ═══════════════════════════════════════════════════════════════════════════════
220
+ # JOB HISTORY STORAGE
221
+ # ═══════════════════════════════════════════════════════════════════════════════
222
+
223
+ JOBS_FILE = CONFIG_DIR / "jobs.json"
224
+ MAX_JOB_HISTORY = 100 # Keep last N jobs
225
+
226
+
227
+ def save_job(job_info: dict) -> None:
228
+ """
229
+ Save a job to the local history.
230
+
231
+ Used by background job submission to track jobs for later status checks.
232
+
233
+ Args:
234
+ job_info: Dict with job_id, call_id, user_id, tool_name, etc.
235
+ """
236
+ from datetime import datetime
237
+ import os
238
+
239
+ # Load existing jobs
240
+ jobs = []
241
+ if JOBS_FILE.exists():
242
+ try:
243
+ jobs = json.loads(JOBS_FILE.read_text())
244
+ except (json.JSONDecodeError, IOError):
245
+ jobs = []
246
+
247
+ # Add timestamp and status
248
+ job_info["submitted_at"] = datetime.now().isoformat()
249
+ job_info["status"] = "submitted"
250
+
251
+ # Add to front of list (most recent first)
252
+ jobs.insert(0, job_info)
253
+
254
+ # Trim to max history
255
+ jobs = jobs[:MAX_JOB_HISTORY]
256
+
257
+ # Ensure config directory exists
258
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True, mode=0o700)
259
+
260
+ # Write atomically with secure permissions
261
+ content = json.dumps(jobs, indent=2)
262
+ flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
263
+ with os.fdopen(os.open(JOBS_FILE, flags, 0o600), "w") as f:
264
+ f.write(content)
265
+
266
+
267
+ def get_job_history(limit: int = 20) -> list[dict]:
268
+ """
269
+ Get recent jobs from local history.
270
+
271
+ Args:
272
+ limit: Maximum number of jobs to return
273
+
274
+ Returns:
275
+ List of job info dicts, most recent first
276
+ """
277
+ if not JOBS_FILE.exists():
278
+ return []
279
+
280
+ try:
281
+ jobs = json.loads(JOBS_FILE.read_text())
282
+ return jobs[:limit]
283
+ except (json.JSONDecodeError, IOError):
284
+ return []
285
+
286
+
287
+ def get_job_info(job_id: str) -> Optional[dict]:
288
+ """
289
+ Look up a job by ID (full or partial match).
290
+
291
+ Args:
292
+ job_id: Full job ID or prefix to match
293
+
294
+ Returns:
295
+ Job info dict or None if not found
296
+ """
297
+ jobs = get_job_history(limit=MAX_JOB_HISTORY)
298
+
299
+ # Try exact match first
300
+ for job in jobs:
301
+ if job.get("job_id") == job_id:
302
+ return job
303
+
304
+ # Try prefix match
305
+ for job in jobs:
306
+ if job.get("job_id", "").startswith(job_id):
307
+ return job
308
+
309
+ return None
310
+
311
+
312
+ def update_job_status(job_id: str, status: str) -> None:
313
+ """
314
+ Update the status of a job in local history.
315
+
316
+ Args:
317
+ job_id: Job ID to update
318
+ status: New status (e.g., "completed", "failed")
319
+ """
320
+ import os
321
+
322
+ if not JOBS_FILE.exists():
323
+ return
324
+
325
+ try:
326
+ jobs = json.loads(JOBS_FILE.read_text())
327
+ except (json.JSONDecodeError, IOError):
328
+ return
329
+
330
+ for job in jobs:
331
+ if job.get("job_id") == job_id:
332
+ job["status"] = status
333
+ break
334
+
335
+ content = json.dumps(jobs, indent=2)
336
+ flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
337
+ with os.fdopen(os.open(JOBS_FILE, flags, 0o600), "w") as f:
338
+ f.write(content)