From 33a6019f27c40547791c3673a217c15405804cd4 Mon Sep 17 00:00:00 2001 From: Chris Lopez Date: Tue, 13 Jan 2026 00:31:14 -0700 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=F0=9F=8C=9F=20add=20workflow=20rer?= =?UTF-8?q?un=20functionality?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement methods to rerun workflow instances from specific steps using either slug or ID. This includes validation and error handling for various scenarios. Also, normalize workflow results for consistent response formatting. --- pyproject.toml | 2 +- tws/_async/client.py | 136 ++++++++++++++++++++++++++++++++++++++++++- tws/_sync/client.py | 134 +++++++++++++++++++++++++++++++++++++++++- tws/base/client.py | 73 +++++++++++++++++++++++ 4 files changed, 340 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ee5d15d..345176d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "tws-sdk" -version = "0.3.0" +version = "0.4.0" description = "TWS client for Python." authors = ["Fireline Science "] homepage = "https://github.com/Fireline-Science/tws-py" diff --git a/tws/_async/client.py b/tws/_async/client.py index fbed8d4..7a22837 100644 --- a/tws/_async/client.py +++ b/tws/_async/client.py @@ -2,7 +2,7 @@ import mimetypes import os import time -from typing import cast, Dict, Optional +from typing import cast, Dict, Optional, Any import aiofiles import httpx @@ -231,6 +231,138 @@ async def run_workflow( instance = result[0] workflow_result = self._handle_workflow_status(instance) if workflow_result is not None: - return workflow_result + return self._normalize_workflow_result(instance, workflow_result) await asyncio.sleep(retry_delay) + + async def get_workflow_steps( + self, + workflow_definition_id: str, + slug: Optional[str] = None, + workflow_step_types: Optional[list[str]] = None, + ): + params = { + "select": "id, slug, type, display_name", + "workflow_definition_id": f"eq.{workflow_definition_id}", + } + + if workflow_step_types is not None: + params["type"] = f"in.({','.join(workflow_step_types)})" + if slug is not None: + params["slug"] = f"eq.{slug}" + + return await self._make_request("GET", f"workflow_steps", params=params) + + async def _rerun_workflow( + self, + workflow_instance_id: str, + start_from_workflow_key: str, + start_from_workflow_step_value: str, + timeout: int, + retry_delay: int, + step_state_overrides: Optional[Dict[str, dict]] = None, + ): + """Rerun a workflow instance from a specific step. + + Args: + workflow_instance_id: The unique identifier of the workflow instance to rerun + start_from_workflow_key: The workflow step key to start from ("start_from_slug_name" or "start_from_workflow_step_id") + start_from_workflow_step_value: The workflow step ID or slug name to start from + step_state_overrides: Optional dictionary mapping step slugs to state overrides + timeout: Maximum time in seconds to wait for workflow completion (1-3600) + retry_delay: Time in seconds between status checks (1-60) + + Returns: + The workflow execution result as a dictionary + + Raises: + ClientException: If the workflow fails, times out, or if invalid parameters are provided + """ + self._validate_workflow_params(timeout, retry_delay) + + payload: dict[str, Any] = { + "workflow_instance_id": workflow_instance_id, + start_from_workflow_key: start_from_workflow_step_value, + } + if step_state_overrides is not None: + payload["step_state_overrides"] = step_state_overrides + + try: + result = await self._make_rpc_request("rerun_workflow_instance", payload) + except httpx.HTTPStatusError as e: + if e.response.status_code == 400: + error_code = e.response.json().get("code") + error_message = e.response.json().get("message", "") + if error_code == "DNIED": + raise ClientException( + "Permission denied to rerun this workflow instance" + ) + elif "not found" in error_message.lower(): + raise ClientException("Workflow instance or step not found") + elif ( + "running" in error_message.lower() + or "pending" in error_message.lower() + ): + raise ClientException( + "Cannot rerun a workflow that is currently running or pending" + ) + raise ClientException(f"HTTP error occurred: {e}") + + new_workflow_instance_id = result["new_workflow_instance_id"] + start_time = time.time() + + while True: + self._check_timeout(start_time, timeout) + + params = {"select": "status,result", "id": f"eq.{new_workflow_instance_id}"} + result = await self._make_request( + "GET", "workflow_instances", params=params + ) + + if not result: + raise ClientException( + f"Workflow instance {new_workflow_instance_id} not found" + ) + + instance = result[0] + workflow_result = self._handle_workflow_status(instance) + if workflow_result is not None: + return self._normalize_workflow_result(instance, workflow_result) + + await asyncio.sleep(retry_delay) + + async def rerun_workflow_with_step_slug( + self, + workflow_instance_id: str, + start_from_slug_name: str, + step_state_overrides: Optional[Dict[str, dict]] = None, + timeout=600, + retry_delay=1, + ): + """Rerun a workflow instance from a specific step by slug.""" + return await self._rerun_workflow( + workflow_instance_id, + "start_from_slug_name", + start_from_slug_name, + timeout, + retry_delay, + step_state_overrides, + ) + + async def rerun_workflow_with_step_id( + self, + workflow_instance_id: str, + start_from_workflow_step_id: str, + step_state_overrides: Optional[Dict[str, dict]] = None, + timeout=600, + retry_delay=1, + ): + """Rerun a workflow instance from a specific step by ID.""" + return await self._rerun_workflow( + workflow_instance_id, + "start_from_workflow_step_id", + start_from_workflow_step_id, + timeout, + retry_delay, + step_state_overrides, + ) diff --git a/tws/_sync/client.py b/tws/_sync/client.py index a9e75dc..1cf8c84 100644 --- a/tws/_sync/client.py +++ b/tws/_sync/client.py @@ -1,6 +1,6 @@ import os import time -from typing import Dict, cast, Optional +from typing import Any, Dict, cast, Optional import httpx from httpx import Client as SyncHttpClient @@ -214,6 +214,136 @@ def run_workflow( instance = result[0] workflow_result = self._handle_workflow_status(instance) if workflow_result is not None: - return workflow_result + return self._normalize_workflow_result(instance, workflow_result) time.sleep(retry_delay) + + def get_workflow_steps( + self, + workflow_definition_id: str, + slug: Optional[str] = None, + workflow_step_types: Optional[list[str]] = None, + ): + params = { + "select": "id, slug, type, display_name", + "workflow_definition_id": f"eq.{workflow_definition_id}", + } + + if workflow_step_types is not None: + params["type"] = f"in.({','.join(workflow_step_types)})" + if slug is not None: + params["slug"] = f"eq.{slug}" + + return self._make_request("GET", f"workflow_steps", params=params) + + def _rerun_workflow( + self, + workflow_instance_id: str, + start_from_workflow_key: str, + start_from_workflow_step_value: str, + timeout: int, + retry_delay: int, + step_state_overrides: Optional[Dict[str, dict]] = None, + ): + """Rerun a workflow instance from a specific step. + + Args: + workflow_instance_id: The unique identifier of the workflow instance to rerun + start_from_workflow_key: The workflow step key to start from ("start_from_slug_name" or "start_from_workflow_step_id") + start_from_workflow_step_value: The workflow step ID or slug name to start from + step_state_overrides: Optional dictionary mapping step slugs to state overrides + timeout: Maximum time in seconds to wait for workflow completion (1-3600) + retry_delay: Time in seconds between status checks (1-60) + + Returns: + The workflow execution result as a dictionary + + Raises: + ClientException: If the workflow fails, times out, or if invalid parameters are provided + """ + self._validate_workflow_params(timeout, retry_delay) + + payload: dict[str, Any] = { + "workflow_instance_id": workflow_instance_id, + start_from_workflow_key: start_from_workflow_step_value, + } + if step_state_overrides is not None: + payload["step_state_overrides"] = step_state_overrides + + try: + result = self._make_rpc_request("rerun_workflow_instance", payload) + except httpx.HTTPStatusError as e: + if e.response.status_code == 400: + error_code = e.response.json().get("code") + error_message = e.response.json().get("message", "") + if error_code == "DNIED": + raise ClientException( + "Permission denied to rerun this workflow instance" + ) + elif "not found" in error_message.lower(): + raise ClientException("Workflow instance or step not found") + elif ( + "running" in error_message.lower() + or "pending" in error_message.lower() + ): + raise ClientException( + "Cannot rerun a workflow that is currently running or pending" + ) + raise ClientException(f"HTTP error occurred: {e}") + + new_workflow_instance_id = result["new_workflow_instance_id"] + start_time = time.time() + + while True: + self._check_timeout(start_time, timeout) + + params = {"select": "status,result", "id": f"eq.{new_workflow_instance_id}"} + result = self._make_request("GET", "workflow_instances", params=params) + + if not result: + raise ClientException( + f"Workflow instance {new_workflow_instance_id} not found" + ) + + instance = result[0] + workflow_result = self._handle_workflow_status(instance) + if workflow_result is not None: + return self._normalize_workflow_result(instance, workflow_result) + + time.sleep(retry_delay) + + def rerun_workflow_with_step_slug( + self, + workflow_instance_id: str, + start_from_slug_name: str, + step_state_overrides: Optional[Dict[str, dict]] = None, + timeout=600, + retry_delay=1, + ): + """Rerun a workflow instance from a specific step by slug.""" + return self._rerun_workflow( + workflow_instance_id, + "start_from_slug_name", + start_from_slug_name, + timeout, + retry_delay, + step_state_overrides, + ) + + def rerun_workflow_with_step_id( + self, + workflow_instance_id: str, + start_from_workflow_step_id: str, + step_state_overrides: Optional[Dict[str, dict]] = None, + timeout=600, + retry_delay=1, + ): + """Rerun a workflow instance from a specific step by ID.""" + return self._rerun_workflow( + workflow_instance_id, + "start_from_workflow_step_id", + start_from_workflow_step_id, + timeout, + retry_delay, + step_state_overrides, + ) diff --git a/tws/base/client.py b/tws/base/client.py index e982dde..c57811f 100644 --- a/tws/base/client.py +++ b/tws/base/client.py @@ -97,6 +97,27 @@ def _check_timeout(start_time: float, timeout: Union[int, float]) -> None: f"Workflow execution timed out after {timeout} seconds" ) + @staticmethod + def _normalize_workflow_result(instance: dict, workflow_result: dict) -> dict: + """Normalize workflow instance data into a standard response format. + + Args: + instance: The workflow instance data from the API + workflow_result: The processed workflow result + + Returns: + A normalized dictionary with standard workflow response fields + """ + return { + "id": instance.get("id"), + "workflow_definition_id": instance.get("workflow_definition_id"), + "created_at": instance.get("created_at"), + "updated_at": instance.get("updated_at"), + "result": workflow_result, + "status": instance.get("status"), + "instance_num": instance.get("instance_num"), + } + @staticmethod def _validate_tags(tags: Optional[Dict[str, str]]) -> None: if tags is not None: @@ -172,3 +193,55 @@ def run_workflow( ClientException: If the workflow fails, times out, or if invalid parameters are provided """ pass + + @abstractmethod + def rerun_workflow_with_step_id( + self, + workflow_instance_id: str, + start_from_workflow_step_id: str, + step_state_overrides: Optional[Dict[str, dict]] = None, + timeout=600, + retry_delay=1, + ) -> Union[dict, Coroutine[Any, Any, dict]]: + """Rerun a workflow instance from a specific step by ID. + + Args: + workflow_instance_id: The unique identifier of the workflow instance to rerun + start_from_workflow_step_id: The workflow step ID to start from + step_state_overrides: Optional dictionary mapping step slugs to state overrides + timeout: Maximum time in seconds to wait for workflow completion (1-3600) + retry_delay: Time in seconds between status checks (1-60) + + Returns: + The workflow execution result as a dictionary + + Raises: + ClientException: If the workflow fails, times out, or if invalid parameters are provided + """ + pass + + @abstractmethod + def rerun_workflow_with_step_slug( + self, + workflow_instance_id: str, + start_from_slug_name: str, + step_state_overrides: Optional[Dict[str, dict]] = None, + timeout=600, + retry_delay=1, + ) -> Union[dict, Coroutine[Any, Any, dict]]: + """Rerun a workflow instance from a specific step by slug. + + Args: + workflow_instance_id: The unique identifier of the workflow instance to rerun + start_from_slug_name: The workflow slug name to start from + step_state_overrides: Optional dictionary mapping step slugs to state overrides + timeout: Maximum time in seconds to wait for workflow completion (1-3600) + retry_delay: Time in seconds between status checks (1-60) + + Returns: + The workflow execution result as a dictionary + + Raises: + ClientException: If the workflow fails, times out, or if invalid parameters are provided + """ + pass From 999861f0995496a98f4cb5535c48523edbff5f4b Mon Sep 17 00:00:00 2001 From: Chris Lopez Date: Tue, 13 Jan 2026 01:56:11 -0700 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=F0=9F=8C=9F=20add=20tests=20for=20?= =?UTF-8?q?rerun=20workflow=20functionality?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit introduces tests for the rerun workflow functionality, including success cases and error handling. It ensures that the rerun process works correctly with step slugs and IDs, and verifies the expected behavior when encountering various error scenarios. --- tests/test_async_client.py | 322 ++++++++++++++++++++++++++++++++++++- tests/test_sync_client.py | 292 ++++++++++++++++++++++++++++++++- 2 files changed, 600 insertions(+), 14 deletions(-) diff --git a/tests/test_async_client.py b/tests/test_async_client.py index 9dd874a..6f25c22 100644 --- a/tests/test_async_client.py +++ b/tests/test_async_client.py @@ -132,7 +132,15 @@ async def test_run_workflow_with_valid_tags(mock_request, mock_rpc, good_async_c # Mock successful completion mock_request.return_value = [ - {"status": "COMPLETED", "result": {"output": "success"}} + { + "id": "instance-123", + "workflow_definition_id": "workflow-id", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:01:00Z", + "status": "COMPLETED", + "result": {"output": "success"}, + "instance_num": 1, + } ] valid_tags = {"userId": "someUserId", "lessonId": "someLessonId"} @@ -151,7 +159,9 @@ async def test_run_workflow_with_valid_tags(mock_request, mock_rpc, good_async_c "tags": valid_tags, }, ) - assert result == {"output": "success"} + assert result["result"] == {"output": "success"} + assert result["status"] == "COMPLETED" + assert result["id"] == "instance-123" @patch("tws._async.client.AsyncClient._upload_file") @@ -196,7 +206,7 @@ async def test_run_workflow_with_files( }, }, ) - assert result == {"output": "success with file"} + assert result["result"] == {"output": "success with file"} @patch("tws._async.client.AsyncClient._make_rpc_request") @@ -241,12 +251,24 @@ async def test_run_workflow_success(mock_request, mock_rpc, good_async_client): # Mock successful completion mock_request.return_value = [ - {"status": "COMPLETED", "result": {"output": "success"}} + { + "id": "instance-123", + "workflow_definition_id": "workflow-id", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:01:00Z", + "status": "COMPLETED", + "result": {"output": "success"}, + "instance_num": 1, + } ] async with good_async_client: result = await good_async_client.run_workflow("workflow-id", {"arg": "value"}) - assert result == {"output": "success"} + assert result["result"] == {"output": "success"} + assert result["status"] == "COMPLETED" + assert result["id"] == "instance-123" + assert result["workflow_definition_id"] == "workflow-id" + assert result["instance_num"] == 1 @patch("tws._async.client.AsyncClient._make_rpc_request") @@ -273,7 +295,7 @@ async def test_run_workflow_success_after_polling( # Verify sleep was called once with retry_delay mock_sleep.assert_called_once_with(1) - assert result == {"output": "success after poll"} + assert result["result"] == {"output": "success after poll"} @patch("tws._async.client.AsyncClient._make_rpc_request") @@ -548,7 +570,7 @@ async def test_run_workflow_with_multiple_files( }, }, ) - assert result == {"output": "success with multiple files"} + assert result["result"] == {"output": "success with multiple files"} @patch("tws._async.client.AsyncClient._upload_file") @@ -595,4 +617,288 @@ async def test_run_workflow_with_files_and_tags( "tags": tags, }, ) - assert result == {"output": "success with file and tags"} + assert result["result"] == {"output": "success with file and tags"} + + +# Tests for get_workflow_steps + + +@patch("tws._async.client.AsyncClient._make_request") +async def test_get_workflow_steps_success(mock_request, good_async_client): + mock_request.return_value = [ + {"id": "step-1", "slug": "step-one", "type": "action", "display_name": "Step One"}, + {"id": "step-2", "slug": "step-two", "type": "condition", "display_name": "Step Two"}, + ] + + async with good_async_client: + result = await good_async_client.get_workflow_steps("workflow-def-123") + + mock_request.assert_called_once_with( + "GET", + "workflow_steps", + params={ + "select": "id, slug, type, display_name", + "workflow_definition_id": "eq.workflow-def-123", + }, + ) + assert len(result) == 2 + assert result[0]["slug"] == "step-one" + + +@patch("tws._async.client.AsyncClient._make_request") +async def test_get_workflow_steps_with_slug_filter(mock_request, good_async_client): + mock_request.return_value = [ + {"id": "step-1", "slug": "step-one", "type": "action", "display_name": "Step One"}, + ] + + async with good_async_client: + result = await good_async_client.get_workflow_steps( + "workflow-def-123", slug="step-one" + ) + + mock_request.assert_called_once_with( + "GET", + "workflow_steps", + params={ + "select": "id, slug, type, display_name", + "workflow_definition_id": "eq.workflow-def-123", + "slug": "eq.step-one", + }, + ) + assert len(result) == 1 + + +@patch("tws._async.client.AsyncClient._make_request") +async def test_get_workflow_steps_with_type_filter(mock_request, good_async_client): + mock_request.return_value = [ + {"id": "step-1", "slug": "step-one", "type": "action", "display_name": "Step One"}, + ] + + async with good_async_client: + result = await good_async_client.get_workflow_steps( + "workflow-def-123", workflow_step_types=["action", "condition"] + ) + + mock_request.assert_called_once_with( + "GET", + "workflow_steps", + params={ + "select": "id, slug, type, display_name", + "workflow_definition_id": "eq.workflow-def-123", + "type": "in.(action,condition)", + }, + ) + + +# Tests for rerun_workflow_with_step_slug + + +@patch("tws._async.client.AsyncClient._make_rpc_request") +@patch("tws._async.client.AsyncClient._make_request") +async def test_rerun_workflow_with_step_slug_success( + mock_request, mock_rpc, good_async_client +): + # Mock successful rerun start + mock_rpc.return_value = {"new_workflow_instance_id": "new-instance-456"} + + # Mock successful completion + mock_request.return_value = [ + { + "id": "new-instance-456", + "workflow_definition_id": "workflow-def-123", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:01:00Z", + "status": "COMPLETED", + "result": {"output": "rerun success"}, + "instance_num": 2, + } + ] + + async with good_async_client: + result = await good_async_client.rerun_workflow_with_step_slug( + "instance-123", "step-slug-name" + ) + + mock_rpc.assert_called_once_with( + "rerun_workflow_instance", + { + "workflow_instance_id": "instance-123", + "start_from_slug_name": "step-slug-name", + }, + ) + assert result["result"] == {"output": "rerun success"} + assert result["id"] == "new-instance-456" + + +@patch("tws._async.client.AsyncClient._make_rpc_request") +@patch("tws._async.client.AsyncClient._make_request") +async def test_rerun_workflow_with_step_slug_with_overrides( + mock_request, mock_rpc, good_async_client +): + mock_rpc.return_value = {"new_workflow_instance_id": "new-instance-456"} + mock_request.return_value = [ + {"status": "COMPLETED", "result": {"output": "rerun with overrides"}} + ] + + step_overrides = {"step-slug": {"key": "new_value"}} + + async with good_async_client: + await good_async_client.rerun_workflow_with_step_slug( + "instance-123", + "step-slug-name", + step_state_overrides=step_overrides, + ) + + mock_rpc.assert_called_once_with( + "rerun_workflow_instance", + { + "workflow_instance_id": "instance-123", + "start_from_slug_name": "step-slug-name", + "step_state_overrides": step_overrides, + }, + ) + + +# Tests for rerun_workflow_with_step_id + + +@patch("tws._async.client.AsyncClient._make_rpc_request") +@patch("tws._async.client.AsyncClient._make_request") +async def test_rerun_workflow_with_step_id_success( + mock_request, mock_rpc, good_async_client +): + mock_rpc.return_value = {"new_workflow_instance_id": "new-instance-456"} + mock_request.return_value = [ + { + "id": "new-instance-456", + "workflow_definition_id": "workflow-def-123", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:01:00Z", + "status": "COMPLETED", + "result": {"output": "rerun by id success"}, + "instance_num": 2, + } + ] + + async with good_async_client: + result = await good_async_client.rerun_workflow_with_step_id( + "instance-123", "step-id-456" + ) + + mock_rpc.assert_called_once_with( + "rerun_workflow_instance", + { + "workflow_instance_id": "instance-123", + "start_from_workflow_step_id": "step-id-456", + }, + ) + assert result["result"] == {"output": "rerun by id success"} + + +# Tests for rerun workflow error handling + + +@patch("tws._async.client.AsyncClient._make_rpc_request") +async def test_rerun_workflow_permission_denied(mock_rpc, good_async_client): + mock_request = Request("POST", "http://example.com") + mock_response = Response(400, request=mock_request) + mock_response._content = b'{"code": "DNIED", "message": "Permission denied"}' + + mock_rpc.side_effect = HTTPStatusError( + "400 Bad Request", request=mock_request, response=mock_response + ) + + with pytest.raises(ClientException) as exc_info: + async with good_async_client: + await good_async_client.rerun_workflow_with_step_slug( + "instance-123", "step-slug" + ) + assert "Permission denied to rerun this workflow instance" in str(exc_info.value) + + +@patch("tws._async.client.AsyncClient._make_rpc_request") +async def test_rerun_workflow_not_found(mock_rpc, good_async_client): + mock_request = Request("POST", "http://example.com") + mock_response = Response(400, request=mock_request) + mock_response._content = b'{"code": "OTHER", "message": "Workflow instance not found"}' + + mock_rpc.side_effect = HTTPStatusError( + "400 Bad Request", request=mock_request, response=mock_response + ) + + with pytest.raises(ClientException) as exc_info: + async with good_async_client: + await good_async_client.rerun_workflow_with_step_id( + "instance-123", "step-id" + ) + assert "Workflow instance or step not found" in str(exc_info.value) + + +@patch("tws._async.client.AsyncClient._make_rpc_request") +async def test_rerun_workflow_already_running(mock_rpc, good_async_client): + mock_request = Request("POST", "http://example.com") + mock_response = Response(400, request=mock_request) + mock_response._content = b'{"code": "OTHER", "message": "Workflow is currently running"}' + + mock_rpc.side_effect = HTTPStatusError( + "400 Bad Request", request=mock_request, response=mock_response + ) + + with pytest.raises(ClientException) as exc_info: + async with good_async_client: + await good_async_client.rerun_workflow_with_step_slug( + "instance-123", "step-slug" + ) + assert "Cannot rerun a workflow that is currently running or pending" in str( + exc_info.value + ) + + +@patch("tws._async.client.AsyncClient._make_rpc_request") +@patch("tws._async.client.AsyncClient._make_request") +@patch("time.time") +async def test_rerun_workflow_timeout( + mock_time, mock_request, mock_rpc, good_async_client +): + mock_rpc.return_value = {"new_workflow_instance_id": "new-instance-456"} + mock_request.return_value = [{"status": "RUNNING", "result": None}] + mock_time.side_effect = [0, 601] + + with pytest.raises(ClientException) as exc_info: + async with good_async_client: + await good_async_client.rerun_workflow_with_step_slug( + "instance-123", "step-slug", timeout=600 + ) + assert "Workflow execution timed out after 600 seconds" in str(exc_info.value) + + +@patch("tws._async.client.AsyncClient._make_rpc_request") +@patch("tws._async.client.AsyncClient._make_request") +async def test_rerun_workflow_instance_not_found_during_poll( + mock_request, mock_rpc, good_async_client +): + mock_rpc.return_value = {"new_workflow_instance_id": "new-instance-456"} + mock_request.return_value = [] + + with pytest.raises(ClientException) as exc_info: + async with good_async_client: + await good_async_client.rerun_workflow_with_step_slug( + "instance-123", "step-slug" + ) + assert "Workflow instance new-instance-456 not found" in str(exc_info.value) + + +@patch("tws._async.client.AsyncClient._make_rpc_request") +@patch("tws._async.client.AsyncClient._make_request") +async def test_rerun_workflow_failure(mock_request, mock_rpc, good_async_client): + mock_rpc.return_value = {"new_workflow_instance_id": "new-instance-456"} + mock_request.return_value = [ + {"status": "FAILED", "result": {"error": "workflow failed"}} + ] + + with pytest.raises(ClientException) as exc_info: + async with good_async_client: + await good_async_client.rerun_workflow_with_step_slug( + "instance-123", "step-slug" + ) + assert "Workflow execution failed" in str(exc_info.value) diff --git a/tests/test_sync_client.py b/tests/test_sync_client.py index 9461a28..9c728fe 100644 --- a/tests/test_sync_client.py +++ b/tests/test_sync_client.py @@ -94,7 +94,15 @@ def test_run_workflow_with_valid_tags(mock_request, mock_rpc, good_client): # Mock successful completion mock_request.return_value = [ - {"status": "COMPLETED", "result": {"output": "success"}} + { + "id": "instance-123", + "workflow_definition_id": "workflow-id", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:01:00Z", + "status": "COMPLETED", + "result": {"output": "success"}, + "instance_num": 1, + } ] valid_tags = {"userId": "someUserId", "lessonId": "someLessonId"} @@ -113,7 +121,9 @@ def test_run_workflow_with_valid_tags(mock_request, mock_rpc, good_client): "tags": valid_tags, }, ) - assert result == {"output": "success"} + assert result["result"] == {"output": "success"} + assert result["status"] == "COMPLETED" + assert result["id"] == "instance-123" @patch("tws._sync.client.SyncClient._make_rpc_request") @@ -156,12 +166,24 @@ def test_run_workflow_success(mock_request, mock_rpc, good_client): # Mock successful completion mock_request.return_value = [ - {"status": "COMPLETED", "result": {"output": "success"}} + { + "id": "instance-123", + "workflow_definition_id": "workflow-id", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:01:00Z", + "status": "COMPLETED", + "result": {"output": "success"}, + "instance_num": 1, + } ] with good_client: result = good_client.run_workflow("workflow-id", {"arg": "value"}) - assert result == {"output": "success"} + assert result["result"] == {"output": "success"} + assert result["status"] == "COMPLETED" + assert result["id"] == "instance-123" + assert result["workflow_definition_id"] == "workflow-id" + assert result["instance_num"] == 1 @patch("tws._sync.client.SyncClient._make_rpc_request") @@ -188,7 +210,7 @@ def test_run_workflow_success_after_polling( # Verify sleep was called once with retry_delay mock_sleep.assert_called_once_with(1) - assert result == {"output": "success after poll"} + assert result["result"] == {"output": "success after poll"} @patch("tws._sync.client.SyncClient._make_rpc_request") @@ -387,7 +409,7 @@ def test_run_workflow_with_file_upload( workflow_args = mock_rpc.call_args[0][1]["request_body"] assert "file_arg" in workflow_args assert workflow_args["file_arg"] == "test-user-123/timestamp-test_file.txt" - assert result == {"output": "success"} + assert result["result"] == {"output": "success"} @patch("os.path.exists") @@ -456,3 +478,261 @@ def test_run_workflow_with_file_upload_error(mock_time, mock_exists, good_client ) assert "File not found: /path/to/nonexistent/file.txt" in str(exc_info.value) + + +# Tests for get_workflow_steps + + +@patch("tws._sync.client.SyncClient._make_request") +def test_get_workflow_steps_success(mock_request, good_client): + mock_request.return_value = [ + {"id": "step-1", "slug": "step-one", "type": "action", "display_name": "Step One"}, + {"id": "step-2", "slug": "step-two", "type": "condition", "display_name": "Step Two"}, + ] + + with good_client: + result = good_client.get_workflow_steps("workflow-def-123") + + mock_request.assert_called_once_with( + "GET", + "workflow_steps", + params={ + "select": "id, slug, type, display_name", + "workflow_definition_id": "eq.workflow-def-123", + }, + ) + assert len(result) == 2 + assert result[0]["slug"] == "step-one" + + +@patch("tws._sync.client.SyncClient._make_request") +def test_get_workflow_steps_with_slug_filter(mock_request, good_client): + mock_request.return_value = [ + {"id": "step-1", "slug": "step-one", "type": "action", "display_name": "Step One"}, + ] + + with good_client: + result = good_client.get_workflow_steps("workflow-def-123", slug="step-one") + + mock_request.assert_called_once_with( + "GET", + "workflow_steps", + params={ + "select": "id, slug, type, display_name", + "workflow_definition_id": "eq.workflow-def-123", + "slug": "eq.step-one", + }, + ) + assert len(result) == 1 + + +@patch("tws._sync.client.SyncClient._make_request") +def test_get_workflow_steps_with_type_filter(mock_request, good_client): + mock_request.return_value = [ + {"id": "step-1", "slug": "step-one", "type": "action", "display_name": "Step One"}, + ] + + with good_client: + result = good_client.get_workflow_steps( + "workflow-def-123", workflow_step_types=["action", "condition"] + ) + + mock_request.assert_called_once_with( + "GET", + "workflow_steps", + params={ + "select": "id, slug, type, display_name", + "workflow_definition_id": "eq.workflow-def-123", + "type": "in.(action,condition)", + }, + ) + + +# Tests for rerun_workflow_with_step_slug + + +@patch("tws._sync.client.SyncClient._make_rpc_request") +@patch("tws._sync.client.SyncClient._make_request") +def test_rerun_workflow_with_step_slug_success(mock_request, mock_rpc, good_client): + # Mock successful rerun start + mock_rpc.return_value = {"new_workflow_instance_id": "new-instance-456"} + + # Mock successful completion + mock_request.return_value = [ + { + "id": "new-instance-456", + "workflow_definition_id": "workflow-def-123", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:01:00Z", + "status": "COMPLETED", + "result": {"output": "rerun success"}, + "instance_num": 2, + } + ] + + with good_client: + result = good_client.rerun_workflow_with_step_slug("instance-123", "step-slug-name") + + mock_rpc.assert_called_once_with( + "rerun_workflow_instance", + { + "workflow_instance_id": "instance-123", + "start_from_slug_name": "step-slug-name", + }, + ) + assert result["result"] == {"output": "rerun success"} + assert result["id"] == "new-instance-456" + + +@patch("tws._sync.client.SyncClient._make_rpc_request") +@patch("tws._sync.client.SyncClient._make_request") +def test_rerun_workflow_with_step_slug_with_overrides(mock_request, mock_rpc, good_client): + mock_rpc.return_value = {"new_workflow_instance_id": "new-instance-456"} + mock_request.return_value = [ + {"status": "COMPLETED", "result": {"output": "rerun with overrides"}} + ] + + step_overrides = {"step-slug": {"key": "new_value"}} + + with good_client: + good_client.rerun_workflow_with_step_slug( + "instance-123", + "step-slug-name", + step_state_overrides=step_overrides, + ) + + mock_rpc.assert_called_once_with( + "rerun_workflow_instance", + { + "workflow_instance_id": "instance-123", + "start_from_slug_name": "step-slug-name", + "step_state_overrides": step_overrides, + }, + ) + + +# Tests for rerun_workflow_with_step_id + + +@patch("tws._sync.client.SyncClient._make_rpc_request") +@patch("tws._sync.client.SyncClient._make_request") +def test_rerun_workflow_with_step_id_success(mock_request, mock_rpc, good_client): + mock_rpc.return_value = {"new_workflow_instance_id": "new-instance-456"} + mock_request.return_value = [ + { + "id": "new-instance-456", + "workflow_definition_id": "workflow-def-123", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:01:00Z", + "status": "COMPLETED", + "result": {"output": "rerun by id success"}, + "instance_num": 2, + } + ] + + with good_client: + result = good_client.rerun_workflow_with_step_id("instance-123", "step-id-456") + + mock_rpc.assert_called_once_with( + "rerun_workflow_instance", + { + "workflow_instance_id": "instance-123", + "start_from_workflow_step_id": "step-id-456", + }, + ) + assert result["result"] == {"output": "rerun by id success"} + + +# Tests for rerun workflow error handling + + +@patch("tws._sync.client.SyncClient._make_rpc_request") +def test_rerun_workflow_permission_denied(mock_rpc, good_client): + mock_request = Request("POST", "http://example.com") + mock_response = Response(400, request=mock_request) + mock_response._content = b'{"code": "DNIED", "message": "Permission denied"}' + + mock_rpc.side_effect = HTTPStatusError( + "400 Bad Request", request=mock_request, response=mock_response + ) + + with pytest.raises(ClientException) as exc_info: + with good_client: + good_client.rerun_workflow_with_step_slug("instance-123", "step-slug") + assert "Permission denied to rerun this workflow instance" in str(exc_info.value) + + +@patch("tws._sync.client.SyncClient._make_rpc_request") +def test_rerun_workflow_not_found(mock_rpc, good_client): + mock_request = Request("POST", "http://example.com") + mock_response = Response(400, request=mock_request) + mock_response._content = b'{"code": "OTHER", "message": "Workflow instance not found"}' + + mock_rpc.side_effect = HTTPStatusError( + "400 Bad Request", request=mock_request, response=mock_response + ) + + with pytest.raises(ClientException) as exc_info: + with good_client: + good_client.rerun_workflow_with_step_id("instance-123", "step-id") + assert "Workflow instance or step not found" in str(exc_info.value) + + +@patch("tws._sync.client.SyncClient._make_rpc_request") +def test_rerun_workflow_already_running(mock_rpc, good_client): + mock_request = Request("POST", "http://example.com") + mock_response = Response(400, request=mock_request) + mock_response._content = b'{"code": "OTHER", "message": "Workflow is currently running"}' + + mock_rpc.side_effect = HTTPStatusError( + "400 Bad Request", request=mock_request, response=mock_response + ) + + with pytest.raises(ClientException) as exc_info: + with good_client: + good_client.rerun_workflow_with_step_slug("instance-123", "step-slug") + assert "Cannot rerun a workflow that is currently running or pending" in str( + exc_info.value + ) + + +@patch("tws._sync.client.SyncClient._make_rpc_request") +@patch("tws._sync.client.SyncClient._make_request") +@patch("time.time") +def test_rerun_workflow_timeout(mock_time, mock_request, mock_rpc, good_client): + mock_rpc.return_value = {"new_workflow_instance_id": "new-instance-456"} + mock_request.return_value = [{"status": "RUNNING", "result": None}] + mock_time.side_effect = [0, 601] + + with pytest.raises(ClientException) as exc_info: + with good_client: + good_client.rerun_workflow_with_step_slug( + "instance-123", "step-slug", timeout=600 + ) + assert "Workflow execution timed out after 600 seconds" in str(exc_info.value) + + +@patch("tws._sync.client.SyncClient._make_rpc_request") +@patch("tws._sync.client.SyncClient._make_request") +def test_rerun_workflow_instance_not_found_during_poll(mock_request, mock_rpc, good_client): + mock_rpc.return_value = {"new_workflow_instance_id": "new-instance-456"} + mock_request.return_value = [] + + with pytest.raises(ClientException) as exc_info: + with good_client: + good_client.rerun_workflow_with_step_slug("instance-123", "step-slug") + assert "Workflow instance new-instance-456 not found" in str(exc_info.value) + + +@patch("tws._sync.client.SyncClient._make_rpc_request") +@patch("tws._sync.client.SyncClient._make_request") +def test_rerun_workflow_failure(mock_request, mock_rpc, good_client): + mock_rpc.return_value = {"new_workflow_instance_id": "new-instance-456"} + mock_request.return_value = [ + {"status": "FAILED", "result": {"error": "workflow failed"}} + ] + + with pytest.raises(ClientException) as exc_info: + with good_client: + good_client.rerun_workflow_with_step_slug("instance-123", "step-slug") + assert "Workflow execution failed" in str(exc_info.value) From de2e6e70d75967577f445d3a619d6b97026eaf47 Mon Sep 17 00:00:00 2001 From: Chris Lopez Date: Tue, 13 Jan 2026 02:32:14 -0700 Subject: [PATCH 3/6] =?UTF-8?q?refactor:=20=F0=9F=94=A5=20remove=20unused?= =?UTF-8?q?=20`get=5Fworkflow=5Fsteps`=20method=20and=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eliminated the `get_workflow_steps` method from both async and sync clients, along with related test cases, as it is no longer required. --- tests/test_async_client.py | 70 -------------------------------------- tests/test_sync_client.py | 68 ------------------------------------ tws/_async/client.py | 18 ---------- tws/_sync/client.py | 18 ---------- 4 files changed, 174 deletions(-) diff --git a/tests/test_async_client.py b/tests/test_async_client.py index 6f25c22..d9ca5eb 100644 --- a/tests/test_async_client.py +++ b/tests/test_async_client.py @@ -620,76 +620,6 @@ async def test_run_workflow_with_files_and_tags( assert result["result"] == {"output": "success with file and tags"} -# Tests for get_workflow_steps - - -@patch("tws._async.client.AsyncClient._make_request") -async def test_get_workflow_steps_success(mock_request, good_async_client): - mock_request.return_value = [ - {"id": "step-1", "slug": "step-one", "type": "action", "display_name": "Step One"}, - {"id": "step-2", "slug": "step-two", "type": "condition", "display_name": "Step Two"}, - ] - - async with good_async_client: - result = await good_async_client.get_workflow_steps("workflow-def-123") - - mock_request.assert_called_once_with( - "GET", - "workflow_steps", - params={ - "select": "id, slug, type, display_name", - "workflow_definition_id": "eq.workflow-def-123", - }, - ) - assert len(result) == 2 - assert result[0]["slug"] == "step-one" - - -@patch("tws._async.client.AsyncClient._make_request") -async def test_get_workflow_steps_with_slug_filter(mock_request, good_async_client): - mock_request.return_value = [ - {"id": "step-1", "slug": "step-one", "type": "action", "display_name": "Step One"}, - ] - - async with good_async_client: - result = await good_async_client.get_workflow_steps( - "workflow-def-123", slug="step-one" - ) - - mock_request.assert_called_once_with( - "GET", - "workflow_steps", - params={ - "select": "id, slug, type, display_name", - "workflow_definition_id": "eq.workflow-def-123", - "slug": "eq.step-one", - }, - ) - assert len(result) == 1 - - -@patch("tws._async.client.AsyncClient._make_request") -async def test_get_workflow_steps_with_type_filter(mock_request, good_async_client): - mock_request.return_value = [ - {"id": "step-1", "slug": "step-one", "type": "action", "display_name": "Step One"}, - ] - - async with good_async_client: - result = await good_async_client.get_workflow_steps( - "workflow-def-123", workflow_step_types=["action", "condition"] - ) - - mock_request.assert_called_once_with( - "GET", - "workflow_steps", - params={ - "select": "id, slug, type, display_name", - "workflow_definition_id": "eq.workflow-def-123", - "type": "in.(action,condition)", - }, - ) - - # Tests for rerun_workflow_with_step_slug diff --git a/tests/test_sync_client.py b/tests/test_sync_client.py index 9c728fe..fad3db4 100644 --- a/tests/test_sync_client.py +++ b/tests/test_sync_client.py @@ -480,74 +480,6 @@ def test_run_workflow_with_file_upload_error(mock_time, mock_exists, good_client assert "File not found: /path/to/nonexistent/file.txt" in str(exc_info.value) -# Tests for get_workflow_steps - - -@patch("tws._sync.client.SyncClient._make_request") -def test_get_workflow_steps_success(mock_request, good_client): - mock_request.return_value = [ - {"id": "step-1", "slug": "step-one", "type": "action", "display_name": "Step One"}, - {"id": "step-2", "slug": "step-two", "type": "condition", "display_name": "Step Two"}, - ] - - with good_client: - result = good_client.get_workflow_steps("workflow-def-123") - - mock_request.assert_called_once_with( - "GET", - "workflow_steps", - params={ - "select": "id, slug, type, display_name", - "workflow_definition_id": "eq.workflow-def-123", - }, - ) - assert len(result) == 2 - assert result[0]["slug"] == "step-one" - - -@patch("tws._sync.client.SyncClient._make_request") -def test_get_workflow_steps_with_slug_filter(mock_request, good_client): - mock_request.return_value = [ - {"id": "step-1", "slug": "step-one", "type": "action", "display_name": "Step One"}, - ] - - with good_client: - result = good_client.get_workflow_steps("workflow-def-123", slug="step-one") - - mock_request.assert_called_once_with( - "GET", - "workflow_steps", - params={ - "select": "id, slug, type, display_name", - "workflow_definition_id": "eq.workflow-def-123", - "slug": "eq.step-one", - }, - ) - assert len(result) == 1 - - -@patch("tws._sync.client.SyncClient._make_request") -def test_get_workflow_steps_with_type_filter(mock_request, good_client): - mock_request.return_value = [ - {"id": "step-1", "slug": "step-one", "type": "action", "display_name": "Step One"}, - ] - - with good_client: - result = good_client.get_workflow_steps( - "workflow-def-123", workflow_step_types=["action", "condition"] - ) - - mock_request.assert_called_once_with( - "GET", - "workflow_steps", - params={ - "select": "id, slug, type, display_name", - "workflow_definition_id": "eq.workflow-def-123", - "type": "in.(action,condition)", - }, - ) - - # Tests for rerun_workflow_with_step_slug diff --git a/tws/_async/client.py b/tws/_async/client.py index 7a22837..66cde50 100644 --- a/tws/_async/client.py +++ b/tws/_async/client.py @@ -235,24 +235,6 @@ async def run_workflow( await asyncio.sleep(retry_delay) - async def get_workflow_steps( - self, - workflow_definition_id: str, - slug: Optional[str] = None, - workflow_step_types: Optional[list[str]] = None, - ): - params = { - "select": "id, slug, type, display_name", - "workflow_definition_id": f"eq.{workflow_definition_id}", - } - - if workflow_step_types is not None: - params["type"] = f"in.({','.join(workflow_step_types)})" - if slug is not None: - params["slug"] = f"eq.{slug}" - - return await self._make_request("GET", f"workflow_steps", params=params) - async def _rerun_workflow( self, workflow_instance_id: str, diff --git a/tws/_sync/client.py b/tws/_sync/client.py index 1cf8c84..396ae6b 100644 --- a/tws/_sync/client.py +++ b/tws/_sync/client.py @@ -218,24 +218,6 @@ def run_workflow( time.sleep(retry_delay) - def get_workflow_steps( - self, - workflow_definition_id: str, - slug: Optional[str] = None, - workflow_step_types: Optional[list[str]] = None, - ): - params = { - "select": "id, slug, type, display_name", - "workflow_definition_id": f"eq.{workflow_definition_id}", - } - - if workflow_step_types is not None: - params["type"] = f"in.({','.join(workflow_step_types)})" - if slug is not None: - params["slug"] = f"eq.{slug}" - - return self._make_request("GET", f"workflow_steps", params=params) - def _rerun_workflow( self, workflow_instance_id: str, From cebebf7f93d45861217bfaa27b411a5e8faa6393 Mon Sep 17 00:00:00 2001 From: Chris Lopez Date: Tue, 13 Jan 2026 02:38:08 -0700 Subject: [PATCH 4/6] =?UTF-8?q?style:=20=E2=9C=8F=EF=B8=8F=20reformat=20te?= =?UTF-8?q?st=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_async_client.py | 8 ++++++-- tests/test_sync_client.py | 20 +++++++++++++++----- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/tests/test_async_client.py b/tests/test_async_client.py index d9ca5eb..0a13628 100644 --- a/tests/test_async_client.py +++ b/tests/test_async_client.py @@ -750,7 +750,9 @@ async def test_rerun_workflow_permission_denied(mock_rpc, good_async_client): async def test_rerun_workflow_not_found(mock_rpc, good_async_client): mock_request = Request("POST", "http://example.com") mock_response = Response(400, request=mock_request) - mock_response._content = b'{"code": "OTHER", "message": "Workflow instance not found"}' + mock_response._content = ( + b'{"code": "OTHER", "message": "Workflow instance not found"}' + ) mock_rpc.side_effect = HTTPStatusError( "400 Bad Request", request=mock_request, response=mock_response @@ -768,7 +770,9 @@ async def test_rerun_workflow_not_found(mock_rpc, good_async_client): async def test_rerun_workflow_already_running(mock_rpc, good_async_client): mock_request = Request("POST", "http://example.com") mock_response = Response(400, request=mock_request) - mock_response._content = b'{"code": "OTHER", "message": "Workflow is currently running"}' + mock_response._content = ( + b'{"code": "OTHER", "message": "Workflow is currently running"}' + ) mock_rpc.side_effect = HTTPStatusError( "400 Bad Request", request=mock_request, response=mock_response diff --git a/tests/test_sync_client.py b/tests/test_sync_client.py index fad3db4..7af5ed2 100644 --- a/tests/test_sync_client.py +++ b/tests/test_sync_client.py @@ -503,7 +503,9 @@ def test_rerun_workflow_with_step_slug_success(mock_request, mock_rpc, good_clie ] with good_client: - result = good_client.rerun_workflow_with_step_slug("instance-123", "step-slug-name") + result = good_client.rerun_workflow_with_step_slug( + "instance-123", "step-slug-name" + ) mock_rpc.assert_called_once_with( "rerun_workflow_instance", @@ -518,7 +520,9 @@ def test_rerun_workflow_with_step_slug_success(mock_request, mock_rpc, good_clie @patch("tws._sync.client.SyncClient._make_rpc_request") @patch("tws._sync.client.SyncClient._make_request") -def test_rerun_workflow_with_step_slug_with_overrides(mock_request, mock_rpc, good_client): +def test_rerun_workflow_with_step_slug_with_overrides( + mock_request, mock_rpc, good_client +): mock_rpc.return_value = {"new_workflow_instance_id": "new-instance-456"} mock_request.return_value = [ {"status": "COMPLETED", "result": {"output": "rerun with overrides"}} @@ -598,7 +602,9 @@ def test_rerun_workflow_permission_denied(mock_rpc, good_client): def test_rerun_workflow_not_found(mock_rpc, good_client): mock_request = Request("POST", "http://example.com") mock_response = Response(400, request=mock_request) - mock_response._content = b'{"code": "OTHER", "message": "Workflow instance not found"}' + mock_response._content = ( + b'{"code": "OTHER", "message": "Workflow instance not found"}' + ) mock_rpc.side_effect = HTTPStatusError( "400 Bad Request", request=mock_request, response=mock_response @@ -614,7 +620,9 @@ def test_rerun_workflow_not_found(mock_rpc, good_client): def test_rerun_workflow_already_running(mock_rpc, good_client): mock_request = Request("POST", "http://example.com") mock_response = Response(400, request=mock_request) - mock_response._content = b'{"code": "OTHER", "message": "Workflow is currently running"}' + mock_response._content = ( + b'{"code": "OTHER", "message": "Workflow is currently running"}' + ) mock_rpc.side_effect = HTTPStatusError( "400 Bad Request", request=mock_request, response=mock_response @@ -646,7 +654,9 @@ def test_rerun_workflow_timeout(mock_time, mock_request, mock_rpc, good_client): @patch("tws._sync.client.SyncClient._make_rpc_request") @patch("tws._sync.client.SyncClient._make_request") -def test_rerun_workflow_instance_not_found_during_poll(mock_request, mock_rpc, good_client): +def test_rerun_workflow_instance_not_found_during_poll( + mock_request, mock_rpc, good_client +): mock_rpc.return_value = {"new_workflow_instance_id": "new-instance-456"} mock_request.return_value = [] From abb15832ffaaa3e150eb625d82257dccc270453f Mon Sep 17 00:00:00 2001 From: Chris Lopez Date: Tue, 13 Jan 2026 12:27:34 -0700 Subject: [PATCH 5/6] =?UTF-8?q?feat:=20=F0=9F=8C=9F=20update=20workflow=20?= =?UTF-8?q?instance=20parameters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enhanced the parameters for fetching workflow instances to include additional fields such as `id`, `workflow_definition_id`, `created_at`, `updated_at`, and `instance_num` for improved data retrieval. --- tws/_async/client.py | 18 ++++++++++-------- tws/_sync/client.py | 10 ++++++++-- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/tws/_async/client.py b/tws/_async/client.py index 66cde50..24406ef 100644 --- a/tws/_async/client.py +++ b/tws/_async/client.py @@ -218,10 +218,11 @@ async def run_workflow( while True: self._check_timeout(start_time, timeout) - params = {"select": "status,result", "id": f"eq.{workflow_instance_id}"} - result = await self._make_request( - "GET", "workflow_instances", params=params - ) + params = { + "select": "id,workflow_definition_id,created_at,updated_at,status,result,instance_num", + "id": f"eq.{workflow_instance_id}", + } + result = await self._make_request("GET", "workflow_instances", params=params) if not result: raise ClientException( @@ -296,10 +297,11 @@ async def _rerun_workflow( while True: self._check_timeout(start_time, timeout) - params = {"select": "status,result", "id": f"eq.{new_workflow_instance_id}"} - result = await self._make_request( - "GET", "workflow_instances", params=params - ) + params = { + "select": "id,workflow_definition_id,created_at,updated_at,status,result,instance_num", + "id": f"eq.{new_workflow_instance_id}", + } + result = await self._make_request("GET", "workflow_instances", params=params) if not result: raise ClientException( diff --git a/tws/_sync/client.py b/tws/_sync/client.py index 396ae6b..3aceccb 100644 --- a/tws/_sync/client.py +++ b/tws/_sync/client.py @@ -203,7 +203,10 @@ def run_workflow( while True: self._check_timeout(start_time, timeout) - params = {"select": "status,result", "id": f"eq.{workflow_instance_id}"} + params = { + "select": "id,workflow_definition_id,created_at,updated_at,status,result,instance_num", + "id": f"eq.{workflow_instance_id}", + } result = self._make_request("GET", "workflow_instances", params=params) if not result: @@ -279,7 +282,10 @@ def _rerun_workflow( while True: self._check_timeout(start_time, timeout) - params = {"select": "status,result", "id": f"eq.{new_workflow_instance_id}"} + params = { + "select": "id,workflow_definition_id,created_at,updated_at,status,result,instance_num", + "id": f"eq.{new_workflow_instance_id}", + } result = self._make_request("GET", "workflow_instances", params=params) if not result: From 5f7518a56cd8763a6968061bb96627fad5db9606 Mon Sep 17 00:00:00 2001 From: Chris Lopez Date: Tue, 13 Jan 2026 12:37:23 -0700 Subject: [PATCH 6/6] =?UTF-8?q?style:=20=E2=9C=8F=EF=B8=8F=20reformat=20re?= =?UTF-8?q?quest=20calls=20in=20client.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tws/_async/client.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tws/_async/client.py b/tws/_async/client.py index 24406ef..a31fdab 100644 --- a/tws/_async/client.py +++ b/tws/_async/client.py @@ -222,7 +222,9 @@ async def run_workflow( "select": "id,workflow_definition_id,created_at,updated_at,status,result,instance_num", "id": f"eq.{workflow_instance_id}", } - result = await self._make_request("GET", "workflow_instances", params=params) + result = await self._make_request( + "GET", "workflow_instances", params=params + ) if not result: raise ClientException( @@ -301,7 +303,9 @@ async def _rerun_workflow( "select": "id,workflow_definition_id,created_at,updated_at,status,result,instance_num", "id": f"eq.{new_workflow_instance_id}", } - result = await self._make_request("GET", "workflow_instances", params=params) + result = await self._make_request( + "GET", "workflow_instances", params=params + ) if not result: raise ClientException(