intelliseq-iflow 0.2.7__tar.gz → 0.2.9__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.
- {intelliseq_iflow-0.2.7 → intelliseq_iflow-0.2.9}/PKG-INFO +1 -1
- {intelliseq_iflow-0.2.7 → intelliseq_iflow-0.2.9}/pyproject.toml +1 -1
- {intelliseq_iflow-0.2.7 → intelliseq_iflow-0.2.9}/src/iflow/__init__.py +1 -1
- {intelliseq_iflow-0.2.7 → intelliseq_iflow-0.2.9}/src/iflow/api.py +14 -2
- {intelliseq_iflow-0.2.7 → intelliseq_iflow-0.2.9}/src/iflow/commands/auth.py +0 -3
- {intelliseq_iflow-0.2.7 → intelliseq_iflow-0.2.9}/src/iflow/commands/config.py +1 -7
- {intelliseq_iflow-0.2.7 → intelliseq_iflow-0.2.9}/src/iflow/commands/files.py +16 -12
- {intelliseq_iflow-0.2.7 → intelliseq_iflow-0.2.9}/src/iflow/commands/runs.py +20 -13
- {intelliseq_iflow-0.2.7 → intelliseq_iflow-0.2.9}/src/iflow/config.py +24 -0
- {intelliseq_iflow-0.2.7 → intelliseq_iflow-0.2.9}/src/iflow/curl.py +17 -9
- {intelliseq_iflow-0.2.7 → intelliseq_iflow-0.2.9}/.gitignore +0 -0
- {intelliseq_iflow-0.2.7 → intelliseq_iflow-0.2.9}/README.md +0 -0
- {intelliseq_iflow-0.2.7 → intelliseq_iflow-0.2.9}/src/iflow/auth.py +0 -0
- {intelliseq_iflow-0.2.7 → intelliseq_iflow-0.2.9}/src/iflow/cli.py +0 -0
- {intelliseq_iflow-0.2.7 → intelliseq_iflow-0.2.9}/src/iflow/commands/__init__.py +0 -0
- {intelliseq_iflow-0.2.7 → intelliseq_iflow-0.2.9}/src/iflow/commands/orders.py +0 -0
- {intelliseq_iflow-0.2.7 → intelliseq_iflow-0.2.9}/src/iflow/commands/pipelines.py +0 -0
- {intelliseq_iflow-0.2.7 → intelliseq_iflow-0.2.9}/tests/__init__.py +0 -0
- {intelliseq_iflow-0.2.7 → intelliseq_iflow-0.2.9}/tests/conftest.py +0 -0
- {intelliseq_iflow-0.2.7 → intelliseq_iflow-0.2.9}/tests/test_cli_workflow.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: intelliseq-iflow
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.9
|
|
4
4
|
Summary: CLI tool for iFlow - genomic data management and workflow execution
|
|
5
5
|
Project-URL: Homepage, https://intelliseq.com
|
|
6
6
|
Project-URL: Documentation, https://docs.iflow.intelliseq.com
|
|
@@ -98,6 +98,14 @@ class RunOutput:
|
|
|
98
98
|
description: str | None = None
|
|
99
99
|
|
|
100
100
|
|
|
101
|
+
@dataclass
|
|
102
|
+
class RunOutputsResponse:
|
|
103
|
+
"""Response from run outputs endpoint."""
|
|
104
|
+
|
|
105
|
+
outputs: list[RunOutput]
|
|
106
|
+
message: str | None = None # Informational message (e.g., why outputs may be empty)
|
|
107
|
+
|
|
108
|
+
|
|
101
109
|
# Miner service models
|
|
102
110
|
|
|
103
111
|
|
|
@@ -480,14 +488,14 @@ class ComputeAPIClient:
|
|
|
480
488
|
data = await self._request("GET", f"/api/v1/runs/by-order/{order_id}")
|
|
481
489
|
return [self._parse_run(r) for r in data]
|
|
482
490
|
|
|
483
|
-
async def get_run_outputs(self, run_id: str) ->
|
|
491
|
+
async def get_run_outputs(self, run_id: str) -> RunOutputsResponse:
|
|
484
492
|
"""Get semantic outputs for a run.
|
|
485
493
|
|
|
486
494
|
Parses metadata.json and maps to semantic names from meta.json.
|
|
487
495
|
Only available for WDL runs that have completed.
|
|
488
496
|
"""
|
|
489
497
|
data = await self._request("GET", f"/api/v1/runs/{run_id}/outputs")
|
|
490
|
-
|
|
498
|
+
outputs = [
|
|
491
499
|
RunOutput(
|
|
492
500
|
name=o["name"],
|
|
493
501
|
display_name=o["display_name"],
|
|
@@ -497,6 +505,10 @@ class ComputeAPIClient:
|
|
|
497
505
|
)
|
|
498
506
|
for o in data.get("outputs", [])
|
|
499
507
|
]
|
|
508
|
+
return RunOutputsResponse(
|
|
509
|
+
outputs=outputs,
|
|
510
|
+
message=data.get("message"),
|
|
511
|
+
)
|
|
500
512
|
|
|
501
513
|
def _parse_run(self, data: dict, project_id: str | None = None) -> Run:
|
|
502
514
|
"""Parse run data from API response.
|
|
@@ -68,9 +68,6 @@ async def auto_configure_after_login(
|
|
|
68
68
|
p = projects[0]
|
|
69
69
|
set_project_context(p.id, p.name, p.org_id, p.org_name, p.bucket_name)
|
|
70
70
|
console.print(f"[dim]Project:[/dim] {p.name} (auto-selected)")
|
|
71
|
-
if p.bucket_name:
|
|
72
|
-
bucket = normalize_bucket_name(p.bucket_name)
|
|
73
|
-
console.print(f"[dim]Bucket:[/dim] gs://{bucket}/")
|
|
74
71
|
elif len(projects) > 1:
|
|
75
72
|
console.print(f"[dim]Found {len(projects)} projects.[/dim]")
|
|
76
73
|
console.print("Run [cyan]iflow config select-project[/cyan] to choose one.")
|
|
@@ -39,9 +39,6 @@ def show_config():
|
|
|
39
39
|
console.print(f" Organization: [cyan]{settings.org_name or 'Unknown'}[/cyan]")
|
|
40
40
|
console.print(f" Project: [cyan]{settings.project_name or 'Unknown'}[/cyan]")
|
|
41
41
|
console.print(f" Project ID: [dim]{settings.project_id}[/dim]")
|
|
42
|
-
if settings.bucket_name:
|
|
43
|
-
bucket = normalize_bucket_name(settings.bucket_name)
|
|
44
|
-
console.print(f" Bucket: [cyan]gs://{bucket}/[/cyan]")
|
|
45
42
|
console.print()
|
|
46
43
|
else:
|
|
47
44
|
console.print("[yellow]No default project set.[/yellow]")
|
|
@@ -258,12 +255,9 @@ def select_project(project_id: str | None, curl: bool):
|
|
|
258
255
|
console.print(f"[green]Selected project:[/green] {selected.name}")
|
|
259
256
|
console.print(f" Organization: {selected.org_name}")
|
|
260
257
|
console.print(f" Project ID: {selected.id}")
|
|
261
|
-
if selected.bucket_name:
|
|
262
|
-
bucket = normalize_bucket_name(selected.bucket_name)
|
|
263
|
-
console.print(f" Bucket: gs://{bucket}/")
|
|
264
258
|
console.print()
|
|
265
259
|
console.print("You can now run commands without specifying -p/--project.")
|
|
266
|
-
console.print("File paths
|
|
260
|
+
console.print("File paths are relative to your project.")
|
|
267
261
|
|
|
268
262
|
except APIError as e:
|
|
269
263
|
console.print(f"[red]Error:[/red] {e}")
|
|
@@ -56,10 +56,11 @@ def list_files(project: str | None, path: str, curl: bool):
|
|
|
56
56
|
try:
|
|
57
57
|
result = asyncio.run(_list())
|
|
58
58
|
|
|
59
|
-
# Display
|
|
60
|
-
console.print(f"[dim]Bucket:[/dim] {result.bucket_name}")
|
|
59
|
+
# Display current path (relative to project)
|
|
61
60
|
if result.path:
|
|
62
61
|
console.print(f"[dim]Path:[/dim] {result.path}")
|
|
62
|
+
else:
|
|
63
|
+
console.print(f"[dim]Path:[/dim] /")
|
|
63
64
|
console.print()
|
|
64
65
|
|
|
65
66
|
if not result.folders and not result.files:
|
|
@@ -157,7 +158,10 @@ def download(
|
|
|
157
158
|
def _download_by_path(project_id: str, path: str, output: str | None, curl: bool):
|
|
158
159
|
"""Download file by direct GCS path."""
|
|
159
160
|
if curl:
|
|
160
|
-
print(files_curl("GET",
|
|
161
|
+
print(files_curl("GET", "/files/download-url", project_id, params={"path": path}))
|
|
162
|
+
print("\n# Then download with curl or wget:")
|
|
163
|
+
print(f"# curl -o '{Path(path).name}' '<url>'")
|
|
164
|
+
print(f"# wget -O '{Path(path).name}' '<url>'")
|
|
161
165
|
return
|
|
162
166
|
|
|
163
167
|
async def _download():
|
|
@@ -240,14 +244,15 @@ def _download_by_output(
|
|
|
240
244
|
|
|
241
245
|
# 4. Get outputs for this run
|
|
242
246
|
console.print("[dim]Getting run outputs...[/dim]")
|
|
243
|
-
|
|
244
|
-
if not outputs:
|
|
245
|
-
|
|
247
|
+
response = await compute_client.get_run_outputs(latest_run.id)
|
|
248
|
+
if not response.outputs:
|
|
249
|
+
msg = response.message or "may not be a WDL run"
|
|
250
|
+
raise APIError(f"No outputs found for run ({msg})")
|
|
246
251
|
|
|
247
252
|
# 5. Find matching output
|
|
248
|
-
matching = [o for o in outputs if o.name == output_name]
|
|
253
|
+
matching = [o for o in response.outputs if o.name == output_name]
|
|
249
254
|
if not matching:
|
|
250
|
-
available = ", ".join(o.name for o in outputs)
|
|
255
|
+
available = ", ".join(o.name for o in response.outputs)
|
|
251
256
|
raise APIError(f"Output '{output_name}' not found. Available: {available}")
|
|
252
257
|
|
|
253
258
|
output_info = matching[0]
|
|
@@ -329,13 +334,12 @@ def upload(project: str | None, file: str, remote_path: str, curl: bool):
|
|
|
329
334
|
if curl:
|
|
330
335
|
print(files_curl(
|
|
331
336
|
"POST",
|
|
332
|
-
"/files/upload",
|
|
337
|
+
"/files/upload-url",
|
|
333
338
|
project_id,
|
|
334
339
|
data={"path": final_remote_path, "content_type": content_type},
|
|
335
340
|
))
|
|
336
|
-
print("\n# Then upload to the
|
|
337
|
-
print(f"# curl -X PUT -H 'Content-Type: {content_type}'
|
|
338
|
-
print(f"# --data-binary @{file} '<signed_url>'")
|
|
341
|
+
print("\n# Then upload to the signed URL from response:")
|
|
342
|
+
print(f"# curl -X PUT -H 'Content-Type: {content_type}' --data-binary @{file} '<url>'")
|
|
339
343
|
return
|
|
340
344
|
|
|
341
345
|
async def _upload():
|
|
@@ -6,7 +6,7 @@ import time
|
|
|
6
6
|
import click
|
|
7
7
|
|
|
8
8
|
from iflow.api import APIError, ComputeAPIClient
|
|
9
|
-
from iflow.config import require_project, resolve_gcs_path
|
|
9
|
+
from iflow.config import require_project, resolve_gcs_path, to_relative_path
|
|
10
10
|
from iflow.curl import compute_curl
|
|
11
11
|
|
|
12
12
|
|
|
@@ -114,7 +114,7 @@ def last_run(
|
|
|
114
114
|
click.echo(run.name)
|
|
115
115
|
elif show_output:
|
|
116
116
|
if run.output_path:
|
|
117
|
-
click.echo(run.output_path)
|
|
117
|
+
click.echo(to_relative_path(run.output_path))
|
|
118
118
|
else:
|
|
119
119
|
click.echo("", err=True) # Empty if no output
|
|
120
120
|
raise SystemExit(1)
|
|
@@ -124,7 +124,7 @@ def last_run(
|
|
|
124
124
|
click.echo(f" ID: {run.id}")
|
|
125
125
|
click.echo(f" Status: {run.status}")
|
|
126
126
|
if run.output_path:
|
|
127
|
-
click.echo(f" Output: {run.output_path}")
|
|
127
|
+
click.echo(f" Output: {to_relative_path(run.output_path)}")
|
|
128
128
|
|
|
129
129
|
except APIError as e:
|
|
130
130
|
click.echo(f"Error: {e}", err=True)
|
|
@@ -293,7 +293,7 @@ def run_status(run_id: str, curl: bool):
|
|
|
293
293
|
click.echo(f" Finished: {run.finished_at}")
|
|
294
294
|
|
|
295
295
|
if run.output_path:
|
|
296
|
-
click.echo(f" Output: {run.output_path}")
|
|
296
|
+
click.echo(f" Output: {to_relative_path(run.output_path)}")
|
|
297
297
|
if run.error_message:
|
|
298
298
|
click.echo(f" Error: {run.error_message}")
|
|
299
299
|
|
|
@@ -354,24 +354,31 @@ def run_outputs(run_id: str, download: str | None, output: str | None, curl: boo
|
|
|
354
354
|
iflow runs outputs RUN_ID -d annotated_vcf_gz
|
|
355
355
|
iflow runs outputs RUN_ID -d top20_tsv -o results.tsv
|
|
356
356
|
"""
|
|
357
|
-
if curl
|
|
357
|
+
if curl:
|
|
358
|
+
# Show curl command regardless of download flag
|
|
358
359
|
print(compute_curl("GET", f"/runs/{run_id}/outputs"))
|
|
360
|
+
if download:
|
|
361
|
+
print("\n# To download a specific output, use the path from the response above with:")
|
|
362
|
+
print("# iflow files download <path> --curl")
|
|
359
363
|
return
|
|
360
364
|
|
|
361
365
|
async def _outputs():
|
|
362
366
|
client = ComputeAPIClient()
|
|
363
367
|
try:
|
|
364
|
-
|
|
368
|
+
response = await client.get_run_outputs(run_id)
|
|
365
369
|
|
|
366
|
-
if not outputs:
|
|
367
|
-
|
|
370
|
+
if not response.outputs:
|
|
371
|
+
if response.message:
|
|
372
|
+
click.echo(f"No outputs: {response.message}")
|
|
373
|
+
else:
|
|
374
|
+
click.echo("No outputs found.")
|
|
368
375
|
return
|
|
369
376
|
|
|
370
377
|
if download:
|
|
371
378
|
# Find output by name
|
|
372
|
-
matching = [o for o in outputs if o.name == download]
|
|
379
|
+
matching = [o for o in response.outputs if o.name == download]
|
|
373
380
|
if not matching:
|
|
374
|
-
available = ", ".join(o.name for o in outputs)
|
|
381
|
+
available = ", ".join(o.name for o in response.outputs)
|
|
375
382
|
click.echo(f"Error: Output '{download}' not found.", err=True)
|
|
376
383
|
click.echo(f"Available outputs: {available}", err=True)
|
|
377
384
|
raise SystemExit(1)
|
|
@@ -414,8 +421,8 @@ def run_outputs(run_id: str, download: str | None, output: str | None, curl: boo
|
|
|
414
421
|
click.echo(f"{'NAME':<20} {'TYPE':<10} {'PATH'}")
|
|
415
422
|
click.echo("-" * 80)
|
|
416
423
|
|
|
417
|
-
for out in outputs:
|
|
418
|
-
path = out.path or "-"
|
|
424
|
+
for out in response.outputs:
|
|
425
|
+
path = to_relative_path(out.path) or "-"
|
|
419
426
|
click.echo(f"{out.name:<20} {out.type:<10} {path}")
|
|
420
427
|
|
|
421
428
|
except APIError as e:
|
|
@@ -472,7 +479,7 @@ async def _watch_run(client: ComputeAPIClient, run_id: str, interval: int = 10):
|
|
|
472
479
|
if run.status == "succeeded":
|
|
473
480
|
click.echo("Run completed successfully!")
|
|
474
481
|
if run.output_path:
|
|
475
|
-
click.echo(f"Output: {run.output_path}")
|
|
482
|
+
click.echo(f"Output: {to_relative_path(run.output_path)}")
|
|
476
483
|
elif run.status == "failed":
|
|
477
484
|
click.echo(f"Run failed: {run.error_message or 'Unknown error'}")
|
|
478
485
|
else:
|
|
@@ -210,6 +210,30 @@ def resolve_gcs_path(path: str) -> str:
|
|
|
210
210
|
return f"gs://{bucket}/{path}"
|
|
211
211
|
|
|
212
212
|
|
|
213
|
+
def to_relative_path(path: str | None) -> str | None:
|
|
214
|
+
"""Convert a full GCS/S3 path to a path relative to project bucket.
|
|
215
|
+
|
|
216
|
+
Strips gs://bucket/ or s3://bucket/ prefix, returning just the path.
|
|
217
|
+
Used for user-facing display where bucket context is implicit.
|
|
218
|
+
|
|
219
|
+
Examples:
|
|
220
|
+
gs://my-bucket/results/file.vcf -> results/file.vcf
|
|
221
|
+
s3://bucket/data/sample.fastq -> data/sample.fastq
|
|
222
|
+
results/file.vcf -> results/file.vcf (already relative)
|
|
223
|
+
"""
|
|
224
|
+
if not path:
|
|
225
|
+
return None
|
|
226
|
+
if path.startswith("gs://"):
|
|
227
|
+
# Format: gs://bucket/path/to/file
|
|
228
|
+
parts = path[5:].split("/", 1)
|
|
229
|
+
return parts[1] if len(parts) > 1 else ""
|
|
230
|
+
if path.startswith("s3://"):
|
|
231
|
+
# Format: s3://bucket/path/to/file
|
|
232
|
+
parts = path[5:].split("/", 1)
|
|
233
|
+
return parts[1] if len(parts) > 1 else ""
|
|
234
|
+
return path # Already relative
|
|
235
|
+
|
|
236
|
+
|
|
213
237
|
def require_project(project_option: str | None) -> str:
|
|
214
238
|
"""Get project ID from option or config, raise if not available."""
|
|
215
239
|
if project_option:
|
|
@@ -27,11 +27,11 @@ def generate_curl(
|
|
|
27
27
|
Returns:
|
|
28
28
|
Complete curl command string
|
|
29
29
|
"""
|
|
30
|
-
parts = ["curl
|
|
30
|
+
parts = ["curl -s"]
|
|
31
31
|
|
|
32
32
|
# Method
|
|
33
33
|
if method != "GET":
|
|
34
|
-
parts.
|
|
34
|
+
parts.append(f"-X {method}")
|
|
35
35
|
|
|
36
36
|
# Add query params to URL
|
|
37
37
|
if params:
|
|
@@ -51,13 +51,16 @@ def generate_curl(
|
|
|
51
51
|
if data and method in ("POST", "PUT", "PATCH"):
|
|
52
52
|
all_headers["Content-Type"] = "application/json"
|
|
53
53
|
|
|
54
|
+
# Combine -H flag with argument to prevent line-break issues
|
|
54
55
|
for key, value in all_headers.items():
|
|
55
|
-
parts.
|
|
56
|
+
parts.append(f"-H '{key}: {value}'")
|
|
56
57
|
|
|
57
|
-
# JSON body
|
|
58
|
+
# JSON body - combine -d flag with argument
|
|
58
59
|
if data:
|
|
59
|
-
json_str = json.dumps(data
|
|
60
|
-
|
|
60
|
+
json_str = json.dumps(data)
|
|
61
|
+
# Escape single quotes in JSON and wrap in single quotes
|
|
62
|
+
json_escaped = json_str.replace("'", "'\\''")
|
|
63
|
+
parts.append(f"-d '{json_escaped}'")
|
|
61
64
|
|
|
62
65
|
# URL (quoted)
|
|
63
66
|
parts.append(shlex.quote(url))
|
|
@@ -86,11 +89,16 @@ def files_curl(
|
|
|
86
89
|
data: dict[str, Any] | None = None,
|
|
87
90
|
params: dict[str, str] | None = None,
|
|
88
91
|
) -> str:
|
|
89
|
-
"""Generate curl for file-service.
|
|
92
|
+
"""Generate curl for file-service.
|
|
93
|
+
|
|
94
|
+
File-service endpoints use /api/v1/files/... with project_id as query param.
|
|
95
|
+
"""
|
|
90
96
|
settings = get_settings()
|
|
91
|
-
url = f"{settings.file_url}/api/v1
|
|
97
|
+
url = f"{settings.file_url}/api/v1{endpoint}"
|
|
92
98
|
headers = {"X-Project-ID": project_id}
|
|
93
|
-
|
|
99
|
+
# Add project_id to query params
|
|
100
|
+
all_params = {"project_id": project_id, **(params or {})}
|
|
101
|
+
return generate_curl(method, url, headers, data, all_params)
|
|
94
102
|
|
|
95
103
|
|
|
96
104
|
def compute_curl(
|
|
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
|