|
| 1 | +import logging |
| 2 | +import sys |
| 3 | +from pathlib import Path |
| 4 | +from time import sleep |
| 5 | +from typing import Annotated |
| 6 | + |
| 7 | +from cyclopts import Parameter, validators |
| 8 | + |
| 9 | +from ..api.get_task_info import GetTaskInfoArgs, api_get_task_info |
| 10 | +from ..library.http_download import download_from_url |
| 11 | +from ..library.invocation_common import ( |
| 12 | + AuditHubContextType, |
| 13 | + OrganizationIdType, |
| 14 | + TaskIdType, |
| 15 | + app, |
| 16 | +) |
| 17 | + |
| 18 | +logger = logging.getLogger(__name__) |
| 19 | + |
| 20 | + |
| 21 | +@app.command |
| 22 | +def download_artifact( |
| 23 | + *, |
| 24 | + organization_id: OrganizationIdType, |
| 25 | + task_id: TaskIdType, |
| 26 | + step_code: str, |
| 27 | + name: str, |
| 28 | + output_file: Path, |
| 29 | + timeout: Annotated[int, Parameter(validator=validators.Number(gt=0))] = 30, |
| 30 | + rpc_context: AuditHubContextType, |
| 31 | +): |
| 32 | + """ |
| 33 | + Download an artifact by name, potentially waiting for it to become available. |
| 34 | +
|
| 35 | + This is an improved version of get-task-artifact, that potentially waits for the artifact to become |
| 36 | + available and downloads it using the provided URL. |
| 37 | +
|
| 38 | + Note that you can use 'ah get-task-info' to obtain the list of step_codes |
| 39 | + (key 'steps') and produced artifacts (key 'artifacts'). |
| 40 | +
|
| 41 | + Also note that, due to the asynchronous nature of AuditHub, artifacts may take a short amount of time |
| 42 | + until they become available, even when the task has finished. |
| 43 | + This is normal, and this command takes this into account. |
| 44 | +
|
| 45 | + Parameters |
| 46 | + ---------- |
| 47 | + step_code: |
| 48 | + The code of the workflow step that produced the artifact |
| 49 | + name: |
| 50 | + The name of the artifact. |
| 51 | + output_file: |
| 52 | + The local file name to store the output in. |
| 53 | + timeout: |
| 54 | + The number of seconds to potentially wait for the artifact to become available. |
| 55 | + """ |
| 56 | + try: |
| 57 | + found = False |
| 58 | + for attempt in range(1, timeout + 1): |
| 59 | + logger.debug("Starting attempt %d", attempt) |
| 60 | + task_info = api_get_task_info( |
| 61 | + rpc_context, |
| 62 | + GetTaskInfoArgs(organization_id=organization_id, task_id=task_id), |
| 63 | + ) |
| 64 | + matched_artifacts = [ |
| 65 | + e |
| 66 | + for e in task_info.get("artifacts", list()) |
| 67 | + if e.get("step_code") == step_code and e.get("name") == name |
| 68 | + ] |
| 69 | + if len(matched_artifacts) > 1: |
| 70 | + # This should be impossible |
| 71 | + raise RuntimeError( |
| 72 | + f"Multiple artifacts matched the condition, bailing: '{matched_artifacts}'" |
| 73 | + ) |
| 74 | + if len(matched_artifacts) == 1: |
| 75 | + logger.debug("Artifact found at attempt %d, downloading...", attempt) |
| 76 | + bytes_written, hr_size = download_from_url( |
| 77 | + matched_artifacts[0]["presigned_url"], output_file |
| 78 | + ) |
| 79 | + logger.info( |
| 80 | + f"Downloaded {bytes_written} bytes ({hr_size}) as {output_file}." |
| 81 | + ) |
| 82 | + found = True |
| 83 | + break |
| 84 | + else: |
| 85 | + logger.info( |
| 86 | + "Artifact not found, waiting a sec at attempt %d..", attempt |
| 87 | + ) |
| 88 | + sleep(1) |
| 89 | + |
| 90 | + if found: |
| 91 | + logger.debug("Finished.") |
| 92 | + else: |
| 93 | + logger.error("Artifact not found (yet?).") |
| 94 | + sys.exit(1) |
| 95 | + |
| 96 | + except Exception as ex: |
| 97 | + logger.error("Error %s", str(ex), exc_info=ex) |
0 commit comments