1515
1616import requests
1717from aignx .codegen .api .public_api import PublicApi
18- from aignx .codegen .exceptions import NotFoundException , ServiceException
18+ from aignx .codegen .exceptions import ApiException , NotFoundException , ServiceException
1919from aignx .codegen .models import (
2020 CustomMetadataUpdateRequest ,
2121 ItemCreationRequest ,
@@ -348,10 +348,72 @@ def download_to_folder( # noqa: C901
348348 msg = f"Download operation failed unexpectedly for run { self .run_id } : { e } "
349349 raise RuntimeError (msg ) from e
350350
351+ @staticmethod
352+ def _fetch_artifact_redirect_url (
353+ endpoint_url : str ,
354+ token : str ,
355+ proxy : str | None ,
356+ ssl_verify : bool | str ,
357+ artifact_id : str ,
358+ ) -> str :
359+ """Execute the HTTP request to the artifact file endpoint and return the redirect URL.
360+
361+ Args:
362+ endpoint_url (str): Full URL of the artifact file endpoint.
363+ token (str): Bearer token for authorization.
364+ proxy (str | None): Optional proxy URL.
365+ ssl_verify (bool | str): SSL verification setting (True/False or CA bundle path).
366+ artifact_id (str): Artifact ID used in error messages.
367+
368+ Returns:
369+ str: The presigned URL from the redirect Location header.
370+
371+ Raises:
372+ NotFoundException: If the artifact is not found (404).
373+ ServiceException: On network errors or 5xx responses.
374+ ApiException: On 4xx client errors.
375+ RuntimeError: If the redirect has no Location header or returns an unexpected status.
376+ """
377+ try :
378+ response = requests .get (
379+ endpoint_url ,
380+ headers = {"Authorization" : f"Bearer { token } " , "User-Agent" : user_agent ()},
381+ allow_redirects = False ,
382+ timeout = settings ().run_timeout ,
383+ proxies = {"http" : proxy , "https" : proxy } if proxy else None ,
384+ verify = ssl_verify ,
385+ )
386+ except requests .Timeout as e :
387+ raise ServiceException (status = HTTPStatus .SERVICE_UNAVAILABLE , reason = "Request timed out" ) from e
388+ except requests .ConnectionError as e :
389+ raise ServiceException (status = HTTPStatus .SERVICE_UNAVAILABLE , reason = "Connection failed" ) from e
390+ except requests .RequestException as e :
391+ raise ServiceException (status = HTTPStatus .SERVICE_UNAVAILABLE , reason = f"Request failed: { e } " ) from e
392+
393+ if response .status_code in {
394+ HTTPStatus .MOVED_PERMANENTLY ,
395+ HTTPStatus .FOUND ,
396+ HTTPStatus .TEMPORARY_REDIRECT ,
397+ HTTPStatus .PERMANENT_REDIRECT ,
398+ }:
399+ location = response .headers .get ("Location" )
400+ if not location :
401+ msg = f"Redirect response { response .status_code } missing Location header for artifact { artifact_id } "
402+ raise RuntimeError (msg )
403+ return location
404+ if response .status_code == HTTPStatus .NOT_FOUND :
405+ raise NotFoundException (status = HTTPStatus .NOT_FOUND , reason = "Artifact not found" )
406+ if response .status_code >= HTTPStatus .INTERNAL_SERVER_ERROR :
407+ raise ServiceException (status = response .status_code , reason = response .reason )
408+ if response .status_code >= HTTPStatus .BAD_REQUEST :
409+ raise ApiException (status = response .status_code , reason = response .reason )
410+ msg = f"Unexpected status { response .status_code } from artifact file endpoint for artifact { artifact_id } "
411+ raise RuntimeError (msg )
412+
351413 def get_artifact_download_url (self , artifact_id : str ) -> str :
352414 """Get a presigned download URL for an artifact via the file endpoint.
353415
354- Calls GET /v1/runs/{run_id}/artifacts/{artifact_id}/file with
416+ Calls GET /api/ v1/runs/{run_id}/artifacts/{artifact_id}/file with
355417 allow_redirects=False and extracts the presigned URL from the Location
356418 header of the 307 redirect response. Using the codegen method is not
357419 viable here because urllib3 follows the redirect automatically, fetching
@@ -365,45 +427,28 @@ def get_artifact_download_url(self, artifact_id: str) -> str:
365427
366428 Raises:
367429 NotFoundException: If the artifact is not found (404).
368- ServiceException: On 5xx server errors (retried automatically).
430+ ServiceException: On 5xx or other unexpected HTTP errors (retried automatically where transient ).
369431 RuntimeError: If the redirect Location header is missing.
370432 """
371- endpoint_url = f"{ self ._api .api_client .configuration .host } /v1/runs/{ self .run_id } /artifacts/{ artifact_id } /file"
372-
373- def _resolve () -> str :
374- token = get_token ()
375- response = requests .get (
376- endpoint_url ,
377- headers = {"Authorization" : f"Bearer { token } " , "User-Agent" : user_agent ()},
378- allow_redirects = False ,
379- timeout = settings ().run_timeout ,
380- )
381- if response .status_code in {
382- HTTPStatus .MOVED_PERMANENTLY ,
383- HTTPStatus .FOUND ,
384- HTTPStatus .TEMPORARY_REDIRECT ,
385- HTTPStatus .PERMANENT_REDIRECT ,
386- }:
387- location = response .headers .get ("Location" )
388- if not location :
389- msg = f"Redirect response { response .status_code } missing Location header for artifact { artifact_id } "
390- raise RuntimeError (msg )
391- return location
392- if response .status_code == HTTPStatus .NOT_FOUND :
393- raise NotFoundException (status = HTTPStatus .NOT_FOUND , reason = "Artifact not found" )
394- if response .status_code >= HTTPStatus .INTERNAL_SERVER_ERROR :
395- raise ServiceException (status = response .status_code , reason = response .reason )
396- response .raise_for_status ()
397- msg = f"Unexpected status { response .status_code } from artifact file endpoint for artifact { artifact_id } "
398- raise RuntimeError (msg )
399-
400- return Retrying (
433+ configuration = self ._api .api_client .configuration
434+ host = configuration .host .rstrip ("/" )
435+ endpoint_url = f"{ host } /api/v1/runs/{ self .run_id } /artifacts/{ artifact_id } /file"
436+ token_provider = getattr (configuration , "token_provider" , None ) or get_token
437+ proxy = getattr (configuration , "proxy" , None )
438+ ssl_ca_cert = getattr (configuration , "ssl_ca_cert" , None )
439+ verify_ssl = getattr (configuration , "verify_ssl" , True )
440+ ssl_verify : bool | str = ssl_ca_cert or verify_ssl
441+ logger .trace ("Resolving download URL for artifact {} via {}" , artifact_id , endpoint_url )
442+
443+ url = Retrying (
401444 retry = retry_if_exception_type (exception_types = RETRYABLE_EXCEPTIONS ),
402445 stop = stop_after_attempt (settings ().run_retry_attempts ),
403446 wait = wait_exponential_jitter (initial = settings ().run_retry_wait_min , max = settings ().run_retry_wait_max ),
404447 before_sleep = _log_retry_attempt ,
405448 reraise = True ,
406- )(_resolve )
449+ )(lambda : self ._fetch_artifact_redirect_url (endpoint_url , token_provider (), proxy , ssl_verify , artifact_id ))
450+ logger .trace ("Resolved download URL for artifact {}" , artifact_id )
451+ return url
407452
408453 def ensure_artifacts_downloaded (
409454 self ,
@@ -431,6 +476,13 @@ def ensure_artifacts_downloaded(
431476 downloaded_at_least_one_artifact = False
432477 for artifact in item .output_artifacts :
433478 if not artifact .output_artifact_id :
479+ logger .warning (
480+ "Skipping artifact {} for item {}: missing output_artifact_id" , artifact .name , item .external_id
481+ )
482+ if print_status :
483+ print (
484+ f"> Skipping artifact { artifact .name } for item { item .external_id } : missing output_artifact_id"
485+ )
434486 continue
435487 item_dir .mkdir (exist_ok = True , parents = True )
436488 file_ending = mime_type_to_file_ending (get_mime_type_for_artifact (artifact ))
0 commit comments