From a37463b6f578a14d8be252fe054b94c397c6b4ec Mon Sep 17 00:00:00 2001 From: Nimisha Shrivastava Date: Wed, 25 Feb 2026 14:28:28 +0530 Subject: [PATCH] Refactored Run_event.py to iterator pattern --- examples/run_events.py | 25 ++- src/pytfe/resources/run_event.py | 30 +--- tests/units/test_run_events.py | 298 +++++++++++++++++++++++++++++++ 3 files changed, 315 insertions(+), 38 deletions(-) create mode 100644 tests/units/test_run_events.py diff --git a/examples/run_events.py b/examples/run_events.py index a648c5b..d8ae612 100644 --- a/examples/run_events.py +++ b/examples/run_events.py @@ -94,23 +94,19 @@ def main(): options = RunEventListOptions(include=include_opts if include_opts else None) try: - event_list = client.run_events.list(args.run_id, options) - - print(f"Total run events: {event_list.total_count or 'N/A'}") - if event_list.current_page and event_list.total_pages: - print(f"Page {event_list.current_page} of {event_list.total_pages}") - print() + event_count = 0 + for event in client.run_events.list(args.run_id, options): + print(f"Event ID: {event.id}") + print(f"Action: {event.action or 'N/A'}") + print(f"Description: {event.description or 'N/A'}") + print(f"Created At: {event.created_at or 'N/A'}") + print() + event_count += 1 - if not event_list.items: + if event_count == 0: print("No run events found for this run.") else: - for event in event_list.items: - print(f"Event ID: {event.id}") - print(f"Action: {event.action or 'N/A'}") - print(f"Description: {event.description or 'N/A'}") - print(f"Created At: {event.created_at or 'N/A'}") - - print() + print(f"Total run events listed: {event_count}") except Exception as e: print(f"Error listing run events: {e}") @@ -139,7 +135,6 @@ def main(): # 3) Summary _print_header("Summary") print(f"Successfully demonstrated run events for run: {args.run_id}") - print(f"Total events found: {event_list.total_count or 'N/A'}") if args.event_id: print(f"Successfully read specific event: {args.event_id}") return 0 diff --git a/src/pytfe/resources/run_event.py b/src/pytfe/resources/run_event.py index fb5479f..7b8ce12 100644 --- a/src/pytfe/resources/run_event.py +++ b/src/pytfe/resources/run_event.py @@ -1,11 +1,11 @@ from __future__ import annotations +from collections.abc import Iterator from typing import Any from ..errors import InvalidRunEventIDError, InvalidRunIDError from ..models.run_event import ( RunEvent, - RunEventList, RunEventListOptions, RunEventReadOptions, ) @@ -16,34 +16,18 @@ class RunEvents(_Service): def list( self, run_id: str, options: RunEventListOptions | None = None - ) -> RunEventList: + ) -> Iterator[RunEvent]: """List all the run events of the given run.""" if not valid_string_id(run_id): raise InvalidRunIDError() params: dict[str, Any] = {} if options and options.include: params["include"] = ",".join(options.include) - r = self.t.request( - "GET", - f"/api/v2/runs/{run_id}/run-events", - 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(RunEvent.model_validate(attrs)) - return RunEventList( - 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"), - ) + path = f"/api/v2/runs/{run_id}/run-events" + for item in self._list(path, params=params): + attrs = item.get("attributes", {}) + attrs["id"] = item.get("id") + yield RunEvent.model_validate(attrs) def read(self, run_event_id: str) -> RunEvent: """Read a specific run event by its ID.""" diff --git a/tests/units/test_run_events.py b/tests/units/test_run_events.py new file mode 100644 index 0000000..af82e78 --- /dev/null +++ b/tests/units/test_run_events.py @@ -0,0 +1,298 @@ +"""Unit tests for the run_events module.""" + +from unittest.mock import Mock, patch + +import pytest + +from pytfe._http import HTTPTransport +from pytfe.errors import InvalidRunEventIDError, InvalidRunIDError +from pytfe.models.run_event import ( + RunEvent, + RunEventIncludeOpt, + RunEventListOptions, + RunEventReadOptions, +) +from pytfe.resources.run_event import RunEvents + + +class TestRunEvents: + """Test the RunEvents service class.""" + + @pytest.fixture + def mock_transport(self): + """Create a mock HTTPTransport.""" + return Mock(spec=HTTPTransport) + + @pytest.fixture + def run_events_service(self, mock_transport): + """Create a RunEvents service with mocked transport.""" + return RunEvents(mock_transport) + + def test_list_run_events_success(self, run_events_service): + """Test successful list operation using iterator pattern.""" + + # Mock data for run events + mock_data = [ + { + "id": "re-123", + "attributes": { + "action": "queued", + "description": "Run queued", + "created-at": "2023-01-01T12:00:00Z", + }, + }, + { + "id": "re-456", + "attributes": { + "action": "planning", + "description": "Planning started", + "created-at": "2023-01-01T12:01:00Z", + }, + }, + { + "id": "re-789", + "attributes": { + "action": "planned", + "description": "Planning finished", + "created-at": "2023-01-01T12:02:00Z", + }, + }, + ] + + with patch.object(run_events_service, "_list") as mock_list: + # Mock _list to return an iterator + mock_list.return_value = iter(mock_data) + + options = RunEventListOptions(include=[RunEventIncludeOpt.RUN_EVENT_ACTOR]) + results = list(run_events_service.list("run-123", options)) + + # Verify _list was called correctly + mock_list.assert_called_once_with( + "/api/v2/runs/run-123/run-events", + params={"include": "actor"}, + ) + + # Verify results + assert len(results) == 3 + assert isinstance(results[0], RunEvent) + assert results[0].id == "re-123" + assert results[0].action == "queued" + assert results[1].id == "re-456" + assert results[1].action == "planning" + assert results[2].id == "re-789" + assert results[2].action == "planned" + + def test_list_run_events_with_multiple_includes(self, run_events_service): + """Test list with multiple include options.""" + + mock_data = [ + { + "id": "re-111", + "attributes": { + "action": "apply-queued", + "description": "Apply queued", + "created-at": "2023-01-01T12:10:00Z", + }, + }, + ] + + with patch.object(run_events_service, "_list") as mock_list: + mock_list.return_value = iter(mock_data) + + options = RunEventListOptions( + include=[ + RunEventIncludeOpt.RUN_EVENT_ACTOR, + RunEventIncludeOpt.RUN_EVENT_COMMENT, + ] + ) + results = list(run_events_service.list("run-456", options)) + + # Verify include parameter is formatted correctly + mock_list.assert_called_once_with( + "/api/v2/runs/run-456/run-events", + params={"include": "actor,comment"}, + ) + + assert len(results) == 1 + assert results[0].id == "re-111" + + def test_list_run_events_no_options(self, run_events_service): + """Test list without include options.""" + + mock_data = [ + { + "id": "re-222", + "attributes": { + "action": "apply-finished", + "created-at": "2023-01-01T12:15:00Z", + }, + }, + ] + + with patch.object(run_events_service, "_list") as mock_list: + mock_list.return_value = iter(mock_data) + + results = list(run_events_service.list("run-789")) + + # Verify _list was called with empty params + mock_list.assert_called_once_with( + "/api/v2/runs/run-789/run-events", + params={}, + ) + + assert len(results) == 1 + assert results[0].id == "re-222" + + def test_list_run_events_empty_result(self, run_events_service): + """Test list with no run events returned.""" + + with patch.object(run_events_service, "_list") as mock_list: + mock_list.return_value = iter([]) + + results = list(run_events_service.list("run-empty")) + + assert len(results) == 0 + + def test_list_run_events_invalid_run_id(self, run_events_service): + """Test list with invalid run ID.""" + + with pytest.raises(InvalidRunIDError): + list(run_events_service.list("")) + + with pytest.raises(InvalidRunIDError): + list(run_events_service.list("run/invalid")) + + def test_read_run_event_success(self, run_events_service): + """Test successful read operation.""" + + mock_response_data = { + "data": { + "id": "re-read-123", + "attributes": { + "action": "planned", + "description": "Run planned successfully", + "created-at": "2023-01-01T13:00:00Z", + }, + } + } + + mock_response = Mock() + mock_response.json.return_value = mock_response_data + + with patch.object(run_events_service, "t") as mock_transport: + mock_transport.request.return_value = mock_response + + result = run_events_service.read("re-read-123") + + # Verify request was made correctly + mock_transport.request.assert_called_once_with( + "GET", + "/api/v2/run-events/re-read-123", + params={}, + ) + + # Verify result + assert isinstance(result, RunEvent) + assert result.id == "re-read-123" + assert result.action == "planned" + assert result.description == "Run planned successfully" + + def test_read_run_event_with_includes(self, run_events_service): + """Test read with include options.""" + + mock_response_data = { + "data": { + "id": "re-read-456", + "attributes": { + "action": "discarded", + "description": "Run discarded", + "created-at": "2023-01-01T13:05:00Z", + }, + } + } + + mock_response = Mock() + mock_response.json.return_value = mock_response_data + + with patch.object(run_events_service, "t") as mock_transport: + mock_transport.request.return_value = mock_response + + options = RunEventReadOptions(include=[RunEventIncludeOpt.RUN_EVENT_ACTOR]) + result = run_events_service.read_with_options("re-read-456", options) + + # Verify include parameter was passed + mock_transport.request.assert_called_once_with( + "GET", + "/api/v2/run-events/re-read-456", + params={"include": "actor"}, + ) + + assert result.id == "re-read-456" + assert result.action == "discarded" + + def test_read_run_event_invalid_id(self, run_events_service): + """Test read with invalid run event ID.""" + + with pytest.raises(InvalidRunEventIDError): + run_events_service.read("") + + with pytest.raises(InvalidRunEventIDError): + run_events_service.read("re/invalid") + + def test_read_vs_read_with_options(self, run_events_service): + """Test that read() delegates to read_with_options().""" + + mock_response_data = { + "data": { + "id": "re-read-789", + "attributes": { + "action": "completed", + "created-at": "2023-01-01T13:10:00Z", + }, + } + } + + mock_response = Mock() + mock_response.json.return_value = mock_response_data + + with patch.object(run_events_service, "t") as mock_transport: + mock_transport.request.return_value = mock_response + + result1 = run_events_service.read("re-read-789") + + # Reset mock + mock_transport.reset_mock() + mock_transport.request.return_value = mock_response + + result2 = run_events_service.read_with_options("re-read-789") + + # Both should produce the same result + assert result1.id == result2.id + assert result1.action == result2.action + + def test_list_run_events_iterator_lazy_loading(self, run_events_service): + """Test that list returns an iterator that lazily loads data.""" + + mock_data = [ + { + "id": "re-lazy-1", + "attributes": { + "action": "queued", + "created-at": "2023-01-01T12:00:00Z", + }, + }, + ] + + with patch.object(run_events_service, "_list") as mock_list: + mock_list.return_value = iter(mock_data) + + # Get the iterator without consuming it yet + iterator = run_events_service.list("run-lazy") + + # _list should not have been called yet (iterator not consumed) + # This test ensures lazy evaluation + first_event = next(iterator) + + # Now _list should have been called + mock_list.assert_called_once() + assert first_event.id == "re-lazy-1"