diff --git a/examples/run.py b/examples/run.py index d95b6e9..96010b0 100644 --- a/examples/run.py +++ b/examples/run.py @@ -67,7 +67,8 @@ def main(): ) try: - run_list = client.runs.list(args.workspace_id, options) + print("running inside run list") + run_list = list(client.runs.list(args.workspace_id, options)) except Exception as e: print(f"Error listing runs: {e}") if args.organization: @@ -75,23 +76,22 @@ def main(): else: return - if "run_list" in locals(): - print(f"Total runs: {run_list.total_count}") - print(f"Page {run_list.current_page} of {run_list.total_pages}") + if "run_list" in locals() and run_list: + print(f"Total runs fetched: {len(run_list)}") print() - for run in run_list.items: + for run in run_list: print(f"- {run.id} | status={run.status} | created={run.created_at}") print(f"message: {run.message}") print(f"has_changes: {run.has_changes} | is_destroy: {run.is_destroy}") - if not run_list.items: + if not run_list: print("No runs found.") else: # 2) Read the most recent run with details _print_header("Reading most recent run details") - latest_run = run_list.items[0] + latest_run = run_list[0] read_options = RunReadOptions( include=[ RunIncludeOpt.RUN_PLAN, @@ -188,10 +188,12 @@ def main(): status="applied,planned,errored", ) - org_runs = client.runs.list_for_organization(args.organization, org_options) - print(f"Found {len(org_runs.items)} runs across organization") + org_runs = list( + client.runs.list_for_organization(args.organization, org_options) + ) + print(f"Found {len(org_runs)} runs across organization") - for run in org_runs.items[:3]: # Show first 3 + for run in org_runs[:3]: # Show first 3 print(f"- {run.id} | status={run.status}") if run.workspace: print(f"workspace: {run.workspace.name}") @@ -204,15 +206,15 @@ def main(): _print_header("Run Actions Demo (Safe Mode)") # Get runs first if not already available - if "run_list" not in locals() or not run_list.items: + if "run_list" not in locals() or not run_list: try: options = RunListOptions(page_size=1) - run_list = client.runs.list(args.workspace_id, options) + run_list = list(client.runs.list(args.workspace_id, options)) except Exception as e: print(f"Error getting runs for actions demo: {e}") return - if not run_list.items: + if not run_list: print("No runs available for actions demo") return diff --git a/src/pytfe/resources/run.py b/src/pytfe/resources/run.py index 49efdbc..4cb430c 100644 --- a/src/pytfe/resources/run.py +++ b/src/pytfe/resources/run.py @@ -1,5 +1,6 @@ from __future__ import annotations +from collections.abc import Iterator from typing import Any from ..errors import ( @@ -10,14 +11,12 @@ TerraformVersionValidForPlanOnlyError, ) from ..models.run import ( - OrganizationRunList, Run, RunApplyOptions, RunCancelOptions, RunCreateOptions, RunDiscardOptions, RunForceCancelOptions, - RunList, RunListForOrganizationOptions, RunListOptions, RunReadOptions, @@ -27,63 +26,33 @@ class Runs(_Service): - def list(self, workspace_id: str, options: RunListOptions | None = None) -> RunList: + def list( + self, workspace_id: str, options: RunListOptions | None = None + ) -> Iterator[Run]: """List all the runs of the given workspace.""" if not valid_string_id(workspace_id): raise InvalidWorkspaceIDError() - params = ( - options.model_dump(by_alias=True, exclude_none=True) if options else None - ) - r = self.t.request( - "GET", - f"/api/v2/workspaces/{workspace_id}/runs", - params=params, - ) - jd = r.json() - items = [] - meta = jd.get("meta", {}) - pagination = meta.get("pagination", {}) - for d in jd.get("data", []): - attrs = d.get("attributes", {}) - attrs["id"] = d.get("id") - items.append(Run.model_validate(attrs)) - return RunList( - items=items, - current_page=pagination.get("current-page"), - total_pages=pagination.get("total-pages"), - prev_page=pagination.get("prev-page"), - next_page=pagination.get("next-page"), - total_count=pagination.get("total-count"), - ) + params = options.model_dump(by_alias=True) if options else {} + path = f"/api/v2/workspaces/{workspace_id}/runs" + for item in self._list(path, params=params): + attrs = item.get("attributes", {}) + attrs["id"] = item.get("id") + yield Run.model_validate(attrs) def list_for_organization( self, organization: str, options: RunListForOrganizationOptions | None = None - ) -> OrganizationRunList: + ) -> Iterator[Run]: """List all the runs of the given organization.""" if not valid_string_id(organization): raise InvalidOrgError() - params = ( - options.model_dump(by_alias=True, exclude_none=True) if options else None - ) - r = self.t.request( - "GET", - f"/api/v2/organizations/{organization}/runs", - params=params, - ) - jd = r.json() - items = [] - meta = jd.get("meta", {}) - pagination = meta.get("pagination", {}) - for d in jd.get("data", []): - attrs = d.get("attributes", {}) - attrs["id"] = d.get("id") - items.append(Run.model_validate(attrs)) - return OrganizationRunList( - items=items, - current_page=pagination.get("current-page"), - prev_page=pagination.get("prev-page"), - next_page=pagination.get("next-page"), - ) + path = f"/api/v2/organizations/{organization}/runs" + params = options.model_dump(by_alias=True, exclude_none=True) if options else {} + # meta = jd.get("meta", {}) + # pagination = meta.get("pagination", {}) + for item in self._list(path, params=params): + attrs = item.get("attributes", {}) + attrs["id"] = item.get("id") + yield Run.model_validate(attrs) def create(self, options: RunCreateOptions) -> Run: """Create a new run for the given workspace.""" diff --git a/tests/units/test_run.py b/tests/units/test_run.py index bce2d2a..c1a9bf6 100644 --- a/tests/units/test_run.py +++ b/tests/units/test_run.py @@ -11,7 +11,6 @@ TerraformVersionValidForPlanOnlyError, ) from pytfe.models.run import ( - OrganizationRunList, Run, RunApplyOptions, RunCancelOptions, @@ -19,7 +18,6 @@ RunDiscardOptions, RunForceCancelOptions, RunIncludeOpt, - RunList, RunListForOrganizationOptions, RunListOptions, RunReadOptions, @@ -47,74 +45,59 @@ def runs_service(self, mock_transport): def test_list_runs_success(self, runs_service): """Test successful list operation.""" - mock_response_data = { - "data": [ - { - "id": "run-123", - "attributes": { - "status": "applied", - "source": "tfe-configuration-version", - "message": "Test run", - "created-at": "2023-01-01T12:00:00Z", - "has-changes": True, - "is-destroy": False, - "auto-apply": False, - "plan-only": False, - }, + mock_list_data = [ + { + "id": "run-123", + "attributes": { + "status": "applied", + "source": "tfe-configuration-version", + "message": "Test run", + "created-at": "2023-01-01T12:00:00Z", + "has-changes": True, + "is-destroy": False, + "auto-apply": False, + "plan-only": False, }, - { - "id": "run-456", - "attributes": { - "status": "planned", - "source": "tfe-ui", - "message": "Another test run", - "created-at": "2023-01-02T14:00:00Z", - "has-changes": False, - "is-destroy": True, - "auto-apply": True, - "plan-only": True, - }, + }, + { + "id": "run-456", + "attributes": { + "status": "planned", + "source": "tfe-ui", + "message": "Another test run", + "created-at": "2023-01-02T14:00:00Z", + "has-changes": False, + "is-destroy": True, + "auto-apply": True, + "plan-only": True, }, - ], - "meta": { - "pagination": { - "current-page": 1, - "total-pages": 2, - "prev-page": None, - "next-page": 2, - "total-count": 10, - } }, - } - - mock_response = Mock() - mock_response.json.return_value = mock_response_data + ] - with patch.object(runs_service, "t") as mock_transport: - mock_transport.request.return_value = mock_response + with patch.object(runs_service, "_list") as mock_list: + mock_list.return_value = mock_list_data - # Test with custom page_size - use a print statement to debug what's actually sent + # Test with options options = RunListOptions(page_number=1, page_size=5) - result = runs_service.list("ws-123", options) + result = list(runs_service.list("ws-123", options)) - # Check what was actually called - call_args = mock_transport.request.call_args - actual_params = call_args[1]["params"] - - # Verify the basic structure - assert call_args[0][0] == "GET" - assert call_args[0][1] == "/api/v2/workspaces/ws-123/runs" - assert actual_params["page[number]"] == 1 - - # Verify result structure - assert isinstance(result, RunList) - assert len(result.items) == 2 - assert result.current_page == 1 - assert result.total_pages == 2 - assert result.total_count == 10 - - # Verify run objects - run1 = result.items[0] + # Verify _list was called with correct path + assert mock_list.call_count == 1 + call_args = mock_list.call_args + assert call_args[0][0] == "/api/v2/workspaces/ws-123/runs" + + # Verify params structure includes pagination and options + params = call_args[1]["params"] + assert "page[number]" in params + assert "page[size]" in params + assert "include" in params + + # Verify result structure - iterator yields Run objects + assert len(result) == 2 + + # Verify run objects were created correctly from response data + run1 = result[0] + assert isinstance(run1, Run) assert run1.id == "run-123" assert run1.status == RunStatus.Run_Applied assert run1.source == RunSource.Run_Source_Configuration_Version @@ -122,64 +105,53 @@ def test_list_runs_success(self, runs_service): assert run1.has_changes is True assert run1.is_destroy is False - run2 = result.items[1] + run2 = result[1] + assert isinstance(run2, Run) assert run2.id == "run-456" assert run2.status == RunStatus.Run_Planned assert run2.source == RunSource.Run_Source_UI + assert run2.message == "Another test run" assert run2.has_changes is False assert run2.is_destroy is True def test_list_for_organization_success(self, runs_service): """Test successful list_for_organization operation.""" - mock_response_data = { - "data": [ - { - "id": "run-org-1", - "attributes": { - "status": "applied", - "source": "tfe-api", - "message": "Organization run", - "created-at": "2023-01-01T12:00:00Z", - "has-changes": True, - "is-destroy": False, - }, - } - ], - "meta": { - "pagination": { - "current-page": 1, - "prev-page": None, - "next-page": None, - } - }, - } - - mock_response = Mock() - mock_response.json.return_value = mock_response_data + mock_response_data = [ + { + "id": "run-org-1", + "attributes": { + "status": "applied", + "source": "tfe-api", + "message": "Organization run", + "created-at": "2023-01-01T12:00:00Z", + "has-changes": True, + "is-destroy": False, + }, + } + ] - with patch.object(runs_service, "t") as mock_transport: - mock_transport.request.return_value = mock_response + with patch.object(runs_service, "_list") as mock_list: + mock_list.return_value = mock_response_data options = RunListForOrganizationOptions(status="applied,planned") - result = runs_service.list_for_organization("test-org", options) + result = list(runs_service.list_for_organization("test-org", options)) - # Verify request was made correctly (account for defaults and aliases) + # Verify _list was called with correct path and params expected_params = { "page[number]": 1, "page[size]": 20, "filter[status]": "applied,planned", "include": [], } - mock_transport.request.assert_called_once_with( - "GET", "/api/v2/organizations/test-org/runs", params=expected_params + mock_list.assert_called_once_with( + "/api/v2/organizations/test-org/runs", params=expected_params ) - # Verify result structure - assert isinstance(result, OrganizationRunList) - assert len(result.items) == 1 - assert result.current_page == 1 - assert result.items[0].id == "run-org-1" + # Verify result structure - now returns list of Run objects + assert len(result) == 1 + assert result[0].id == "run-org-1" + assert result[0].status == RunStatus.Run_Applied def test_create_run_validation_errors(self, runs_service): """Test create method with validation errors."""