From 1e656d512d9524c4aa999eb42329a1fd6143ae89 Mon Sep 17 00:00:00 2001 From: Sam Minot Date: Fri, 22 May 2026 14:45:32 -0700 Subject: [PATCH 1/3] Get task files, work_dir, exit_code, and hash --- .../v1/api/execution/get_task_files.py | 196 ++++++++++++++++++ cirro_api_client/v1/models/__init__.py | 4 + cirro_api_client/v1/models/task.py | 60 ++++++ cirro_api_client/v1/models/task_file.py | 86 ++++++++ .../v1/models/task_files_response.py | 65 ++++++ 5 files changed, 411 insertions(+) create mode 100644 cirro_api_client/v1/api/execution/get_task_files.py create mode 100644 cirro_api_client/v1/models/task_file.py create mode 100644 cirro_api_client/v1/models/task_files_response.py diff --git a/cirro_api_client/v1/api/execution/get_task_files.py b/cirro_api_client/v1/api/execution/get_task_files.py new file mode 100644 index 0000000..e186a52 --- /dev/null +++ b/cirro_api_client/v1/api/execution/get_task_files.py @@ -0,0 +1,196 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import Client +from ...models.task_files_response import TaskFilesResponse +from ...types import Response + + +def _get_kwargs( + project_id: str, + dataset_id: str, + task_id: str, +) -> dict[str, Any]: + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/projects/{project_id}/execution/{dataset_id}/tasks/{task_id}/files".format( + project_id=quote(str(project_id), safe=""), + dataset_id=quote(str(dataset_id), safe=""), + task_id=quote(str(task_id), safe=""), + ), + } + + return _kwargs + + +def _parse_response(*, client: Client, response: httpx.Response) -> TaskFilesResponse | None: + if response.status_code == 200: + return TaskFilesResponse.from_dict(response.json()) + + errors.handle_error_response(response, client.raise_on_unexpected_status) + + +def _build_response(*, client: Client, response: httpx.Response) -> Response[TaskFilesResponse | None]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + project_id: str, + dataset_id: str, + task_id: str, + *, + client: Client, +) -> Response[TaskFilesResponse | None]: + """Get task input and output files + + Gets the input and output files for an individual task. + Returns 400 for non-Nextflow executions and 404 if the task or + work directory is not found. + + Args: + project_id (str): + dataset_id (str): + task_id (str): + client (Client): instance of the API client + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[TaskFilesResponse | None] + """ + + kwargs = _get_kwargs( + project_id=project_id, + dataset_id=dataset_id, + task_id=task_id, + ) + + response = client.get_httpx_client().request( + auth=client.get_auth(), + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + project_id: str, + dataset_id: str, + task_id: str, + *, + client: Client, +) -> TaskFilesResponse | None: + """Get task input and output files + + Gets the input and output files for an individual task. + Returns 400 for non-Nextflow executions and 404 if the task or + work directory is not found. + + Args: + project_id (str): + dataset_id (str): + task_id (str): + client (Client): instance of the API client + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + TaskFilesResponse | None + """ + + try: + return sync_detailed( + project_id=project_id, + dataset_id=dataset_id, + task_id=task_id, + client=client, + ).parsed + except (errors.NotFoundException, errors.BadRequestException): + return None + + +async def asyncio_detailed( + project_id: str, + dataset_id: str, + task_id: str, + *, + client: Client, +) -> Response[TaskFilesResponse | None]: + """Get task input and output files + + Gets the input and output files for an individual task. + + Args: + project_id (str): + dataset_id (str): + task_id (str): + client (Client): instance of the API client + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[TaskFilesResponse | None] + """ + + kwargs = _get_kwargs( + project_id=project_id, + dataset_id=dataset_id, + task_id=task_id, + ) + + response = await client.get_async_httpx_client().request(auth=client.get_auth(), **kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + project_id: str, + dataset_id: str, + task_id: str, + *, + client: Client, +) -> TaskFilesResponse | None: + """Get task input and output files + + Gets the input and output files for an individual task. + + Args: + project_id (str): + dataset_id (str): + task_id (str): + client (Client): instance of the API client + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + TaskFilesResponse | None + """ + + try: + return ( + await asyncio_detailed( + project_id=project_id, + dataset_id=dataset_id, + task_id=task_id, + client=client, + ) + ).parsed + except (errors.NotFoundException, errors.BadRequestException): + return None diff --git a/cirro_api_client/v1/models/__init__.py b/cirro_api_client/v1/models/__init__.py index 8e010a6..064888b 100644 --- a/cirro_api_client/v1/models/__init__.py +++ b/cirro_api_client/v1/models/__init__.py @@ -209,6 +209,8 @@ from .tag import Tag from .task import Task from .task_cost import TaskCost +from .task_file import TaskFile +from .task_files_response import TaskFilesResponse from .tenant_info import TenantInfo from .trigger_ingest_request import TriggerIngestRequest from .update_dataset_request import UpdateDatasetRequest @@ -447,6 +449,8 @@ "Tag", "Task", "TaskCost", + "TaskFile", + "TaskFilesResponse", "TenantInfo", "TriggerIngestRequest", "UpdateDatasetRequest", diff --git a/cirro_api_client/v1/models/task.py b/cirro_api_client/v1/models/task.py index 2399324..09ac6ec 100644 --- a/cirro_api_client/v1/models/task.py +++ b/cirro_api_client/v1/models/task.py @@ -27,6 +27,9 @@ class Task: container_image (None | str | Unset): command_line (None | str | Unset): log_location (None | str | Unset): + work_dir (None | str | Unset): S3 URI of the task's work directory + exit_code (int | None | Unset): Process exit code + hash (None | str | Unset): Short hash prefix used by Nextflow, e.g. ``99/b42c07`` """ name: str @@ -39,6 +42,9 @@ class Task: container_image: None | str | Unset = UNSET command_line: None | str | Unset = UNSET log_location: None | str | Unset = UNSET + work_dir: None | str | Unset = UNSET + exit_code: int | None | Unset = UNSET + hash: None | str | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -100,6 +106,24 @@ def to_dict(self) -> dict[str, Any]: else: log_location = self.log_location + work_dir: None | str | Unset + if isinstance(self.work_dir, Unset): + work_dir = UNSET + else: + work_dir = self.work_dir + + exit_code: int | None | Unset + if isinstance(self.exit_code, Unset): + exit_code = UNSET + else: + exit_code = self.exit_code + + hash_: None | str | Unset + if isinstance(self.hash, Unset): + hash_ = UNSET + else: + hash_ = self.hash + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -124,6 +148,12 @@ def to_dict(self) -> dict[str, Any]: field_dict["commandLine"] = command_line if log_location is not UNSET: field_dict["logLocation"] = log_location + if work_dir is not UNSET: + field_dict["workDir"] = work_dir + if exit_code is not UNSET: + field_dict["exitCode"] = exit_code + if hash_ is not UNSET: + field_dict["hash"] = hash_ return field_dict @@ -230,6 +260,33 @@ def _parse_log_location(data: object) -> None | str | Unset: log_location = _parse_log_location(d.pop("logLocation", UNSET)) + def _parse_work_dir(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + work_dir = _parse_work_dir(d.pop("workDir", UNSET)) + + def _parse_exit_code(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return int(data) + + exit_code = _parse_exit_code(d.pop("exitCode", UNSET)) + + def _parse_hash(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + hash_ = _parse_hash(d.pop("hash", UNSET)) + task = cls( name=name, status=status, @@ -241,6 +298,9 @@ def _parse_log_location(data: object) -> None | str | Unset: container_image=container_image, command_line=command_line, log_location=log_location, + work_dir=work_dir, + exit_code=exit_code, + hash=hash_, ) task.additional_properties = d diff --git a/cirro_api_client/v1/models/task_file.py b/cirro_api_client/v1/models/task_file.py new file mode 100644 index 0000000..d9be6be --- /dev/null +++ b/cirro_api_client/v1/models/task_file.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="TaskFile") + + +@_attrs_define +class TaskFile: + """ + Attributes: + uri (str): S3 URI of the file + size (int | None | Unset): File size in bytes + source_task (str | None | Unset): native_id of the task that produced this file, + or None if it came from outside the dataset (input files only) + """ + + uri: str + size: int | None | Unset = UNSET + source_task: str | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + uri = self.uri + + size: int | None | Unset + if isinstance(self.size, Unset): + size = UNSET + else: + size = self.size + + source_task: str | None | Unset + if isinstance(self.source_task, Unset): + source_task = UNSET + else: + source_task = self.source_task + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({"uri": uri}) + if size is not UNSET: + field_dict["size"] = size + if source_task is not UNSET: + field_dict["sourceTask"] = source_task + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + uri = d.pop("uri") + + size_data = d.pop("size", UNSET) + size: int | None | Unset + if size_data is None or isinstance(size_data, Unset): + size = size_data + else: + size = int(size_data) + + source_task: str | None | Unset = d.pop("sourceTask", UNSET) + + task_file = cls(uri=uri, size=size, source_task=source_task) + task_file.additional_properties = d + return task_file + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/cirro_api_client/v1/models/task_files_response.py b/cirro_api_client/v1/models/task_files_response.py new file mode 100644 index 0000000..f7072a7 --- /dev/null +++ b/cirro_api_client/v1/models/task_files_response.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.task_file import TaskFile + +T = TypeVar("T", bound="TaskFilesResponse") + + +@_attrs_define +class TaskFilesResponse: + """ + Attributes: + input_files (list[TaskFile]): Input files for the task + output_files (list[TaskFile]): Output files for the task + """ + + input_files: list["TaskFile"] + output_files: list["TaskFile"] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + input_files = [f.to_dict() for f in self.input_files] + output_files = [f.to_dict() for f in self.output_files] + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "inputFiles": input_files, + "outputFiles": output_files, + }) + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.task_file import TaskFile + + d = dict(src_dict) + input_files = [TaskFile.from_dict(item) for item in d.pop("inputFiles", [])] + output_files = [TaskFile.from_dict(item) for item in d.pop("outputFiles", [])] + + obj = cls(input_files=input_files, output_files=output_files) + obj.additional_properties = d + return obj + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties From cbe4e54ef82f947f14db0f1844ebe60ded17c7b1 Mon Sep 17 00:00:00 2001 From: Sam Minot Date: Fri, 22 May 2026 14:50:53 -0700 Subject: [PATCH 2/3] Skip type checking --- cirro_api_client/v1/models/task.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cirro_api_client/v1/models/task.py b/cirro_api_client/v1/models/task.py index 09ac6ec..0a510aa 100644 --- a/cirro_api_client/v1/models/task.py +++ b/cirro_api_client/v1/models/task.py @@ -274,7 +274,7 @@ def _parse_exit_code(data: object) -> int | None | Unset: return data if isinstance(data, Unset): return data - return int(data) + return int(data) # type: ignore[call-overload] exit_code = _parse_exit_code(d.pop("exitCode", UNSET)) From 7687cb3f3bb325278c213d93b94234cd87ca9a35 Mon Sep 17 00:00:00 2001 From: Sam Minot Date: Fri, 22 May 2026 14:52:48 -0700 Subject: [PATCH 3/3] Bump version to 1.5.0 Co-Authored-By: Claude Sonnet 4.6 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 70016d1..2f9ba5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "cirro_api_client" -version = "1.4.0" +version = "1.5.0" description = "A client library for accessing Cirro" authors = ["Cirro "] license = "MIT"