Skip to content

Commit b3a8acb

Browse files
authored
Merge pull request #97 from NimishaShrivastava-dev/refactoring-iterator-run-event
Refactored Run_event.py to iterator pattern
2 parents 5a8ce85 + a37463b commit b3a8acb

3 files changed

Lines changed: 315 additions & 38 deletions

File tree

examples/run_events.py

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -94,23 +94,19 @@ def main():
9494
options = RunEventListOptions(include=include_opts if include_opts else None)
9595

9696
try:
97-
event_list = client.run_events.list(args.run_id, options)
98-
99-
print(f"Total run events: {event_list.total_count or 'N/A'}")
100-
if event_list.current_page and event_list.total_pages:
101-
print(f"Page {event_list.current_page} of {event_list.total_pages}")
102-
print()
97+
event_count = 0
98+
for event in client.run_events.list(args.run_id, options):
99+
print(f"Event ID: {event.id}")
100+
print(f"Action: {event.action or 'N/A'}")
101+
print(f"Description: {event.description or 'N/A'}")
102+
print(f"Created At: {event.created_at or 'N/A'}")
103+
print()
104+
event_count += 1
103105

104-
if not event_list.items:
106+
if event_count == 0:
105107
print("No run events found for this run.")
106108
else:
107-
for event in event_list.items:
108-
print(f"Event ID: {event.id}")
109-
print(f"Action: {event.action or 'N/A'}")
110-
print(f"Description: {event.description or 'N/A'}")
111-
print(f"Created At: {event.created_at or 'N/A'}")
112-
113-
print()
109+
print(f"Total run events listed: {event_count}")
114110

115111
except Exception as e:
116112
print(f"Error listing run events: {e}")
@@ -139,7 +135,6 @@ def main():
139135
# 3) Summary
140136
_print_header("Summary")
141137
print(f"Successfully demonstrated run events for run: {args.run_id}")
142-
print(f"Total events found: {event_list.total_count or 'N/A'}")
143138
if args.event_id:
144139
print(f"Successfully read specific event: {args.event_id}")
145140
return 0

src/pytfe/resources/run_event.py

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
from __future__ import annotations
22

3+
from collections.abc import Iterator
34
from typing import Any
45

56
from ..errors import InvalidRunEventIDError, InvalidRunIDError
67
from ..models.run_event import (
78
RunEvent,
8-
RunEventList,
99
RunEventListOptions,
1010
RunEventReadOptions,
1111
)
@@ -16,34 +16,18 @@
1616
class RunEvents(_Service):
1717
def list(
1818
self, run_id: str, options: RunEventListOptions | None = None
19-
) -> RunEventList:
19+
) -> Iterator[RunEvent]:
2020
"""List all the run events of the given run."""
2121
if not valid_string_id(run_id):
2222
raise InvalidRunIDError()
2323
params: dict[str, Any] = {}
2424
if options and options.include:
2525
params["include"] = ",".join(options.include)
26-
r = self.t.request(
27-
"GET",
28-
f"/api/v2/runs/{run_id}/run-events",
29-
params=params,
30-
)
31-
jd = r.json()
32-
items = []
33-
meta = jd.get("meta", {})
34-
pagination = meta.get("pagination", {})
35-
for d in jd.get("data", []):
36-
attrs = d.get("attributes", {})
37-
attrs["id"] = d.get("id")
38-
items.append(RunEvent.model_validate(attrs))
39-
return RunEventList(
40-
items=items,
41-
current_page=pagination.get("current-page"),
42-
total_pages=pagination.get("total-pages"),
43-
prev_page=pagination.get("prev-page"),
44-
next_page=pagination.get("next-page"),
45-
total_count=pagination.get("total-count"),
46-
)
26+
path = f"/api/v2/runs/{run_id}/run-events"
27+
for item in self._list(path, params=params):
28+
attrs = item.get("attributes", {})
29+
attrs["id"] = item.get("id")
30+
yield RunEvent.model_validate(attrs)
4731

4832
def read(self, run_event_id: str) -> RunEvent:
4933
"""Read a specific run event by its ID."""

tests/units/test_run_events.py

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
"""Unit tests for the run_events module."""
2+
3+
from unittest.mock import Mock, patch
4+
5+
import pytest
6+
7+
from pytfe._http import HTTPTransport
8+
from pytfe.errors import InvalidRunEventIDError, InvalidRunIDError
9+
from pytfe.models.run_event import (
10+
RunEvent,
11+
RunEventIncludeOpt,
12+
RunEventListOptions,
13+
RunEventReadOptions,
14+
)
15+
from pytfe.resources.run_event import RunEvents
16+
17+
18+
class TestRunEvents:
19+
"""Test the RunEvents service class."""
20+
21+
@pytest.fixture
22+
def mock_transport(self):
23+
"""Create a mock HTTPTransport."""
24+
return Mock(spec=HTTPTransport)
25+
26+
@pytest.fixture
27+
def run_events_service(self, mock_transport):
28+
"""Create a RunEvents service with mocked transport."""
29+
return RunEvents(mock_transport)
30+
31+
def test_list_run_events_success(self, run_events_service):
32+
"""Test successful list operation using iterator pattern."""
33+
34+
# Mock data for run events
35+
mock_data = [
36+
{
37+
"id": "re-123",
38+
"attributes": {
39+
"action": "queued",
40+
"description": "Run queued",
41+
"created-at": "2023-01-01T12:00:00Z",
42+
},
43+
},
44+
{
45+
"id": "re-456",
46+
"attributes": {
47+
"action": "planning",
48+
"description": "Planning started",
49+
"created-at": "2023-01-01T12:01:00Z",
50+
},
51+
},
52+
{
53+
"id": "re-789",
54+
"attributes": {
55+
"action": "planned",
56+
"description": "Planning finished",
57+
"created-at": "2023-01-01T12:02:00Z",
58+
},
59+
},
60+
]
61+
62+
with patch.object(run_events_service, "_list") as mock_list:
63+
# Mock _list to return an iterator
64+
mock_list.return_value = iter(mock_data)
65+
66+
options = RunEventListOptions(include=[RunEventIncludeOpt.RUN_EVENT_ACTOR])
67+
results = list(run_events_service.list("run-123", options))
68+
69+
# Verify _list was called correctly
70+
mock_list.assert_called_once_with(
71+
"/api/v2/runs/run-123/run-events",
72+
params={"include": "actor"},
73+
)
74+
75+
# Verify results
76+
assert len(results) == 3
77+
assert isinstance(results[0], RunEvent)
78+
assert results[0].id == "re-123"
79+
assert results[0].action == "queued"
80+
assert results[1].id == "re-456"
81+
assert results[1].action == "planning"
82+
assert results[2].id == "re-789"
83+
assert results[2].action == "planned"
84+
85+
def test_list_run_events_with_multiple_includes(self, run_events_service):
86+
"""Test list with multiple include options."""
87+
88+
mock_data = [
89+
{
90+
"id": "re-111",
91+
"attributes": {
92+
"action": "apply-queued",
93+
"description": "Apply queued",
94+
"created-at": "2023-01-01T12:10:00Z",
95+
},
96+
},
97+
]
98+
99+
with patch.object(run_events_service, "_list") as mock_list:
100+
mock_list.return_value = iter(mock_data)
101+
102+
options = RunEventListOptions(
103+
include=[
104+
RunEventIncludeOpt.RUN_EVENT_ACTOR,
105+
RunEventIncludeOpt.RUN_EVENT_COMMENT,
106+
]
107+
)
108+
results = list(run_events_service.list("run-456", options))
109+
110+
# Verify include parameter is formatted correctly
111+
mock_list.assert_called_once_with(
112+
"/api/v2/runs/run-456/run-events",
113+
params={"include": "actor,comment"},
114+
)
115+
116+
assert len(results) == 1
117+
assert results[0].id == "re-111"
118+
119+
def test_list_run_events_no_options(self, run_events_service):
120+
"""Test list without include options."""
121+
122+
mock_data = [
123+
{
124+
"id": "re-222",
125+
"attributes": {
126+
"action": "apply-finished",
127+
"created-at": "2023-01-01T12:15:00Z",
128+
},
129+
},
130+
]
131+
132+
with patch.object(run_events_service, "_list") as mock_list:
133+
mock_list.return_value = iter(mock_data)
134+
135+
results = list(run_events_service.list("run-789"))
136+
137+
# Verify _list was called with empty params
138+
mock_list.assert_called_once_with(
139+
"/api/v2/runs/run-789/run-events",
140+
params={},
141+
)
142+
143+
assert len(results) == 1
144+
assert results[0].id == "re-222"
145+
146+
def test_list_run_events_empty_result(self, run_events_service):
147+
"""Test list with no run events returned."""
148+
149+
with patch.object(run_events_service, "_list") as mock_list:
150+
mock_list.return_value = iter([])
151+
152+
results = list(run_events_service.list("run-empty"))
153+
154+
assert len(results) == 0
155+
156+
def test_list_run_events_invalid_run_id(self, run_events_service):
157+
"""Test list with invalid run ID."""
158+
159+
with pytest.raises(InvalidRunIDError):
160+
list(run_events_service.list(""))
161+
162+
with pytest.raises(InvalidRunIDError):
163+
list(run_events_service.list("run/invalid"))
164+
165+
def test_read_run_event_success(self, run_events_service):
166+
"""Test successful read operation."""
167+
168+
mock_response_data = {
169+
"data": {
170+
"id": "re-read-123",
171+
"attributes": {
172+
"action": "planned",
173+
"description": "Run planned successfully",
174+
"created-at": "2023-01-01T13:00:00Z",
175+
},
176+
}
177+
}
178+
179+
mock_response = Mock()
180+
mock_response.json.return_value = mock_response_data
181+
182+
with patch.object(run_events_service, "t") as mock_transport:
183+
mock_transport.request.return_value = mock_response
184+
185+
result = run_events_service.read("re-read-123")
186+
187+
# Verify request was made correctly
188+
mock_transport.request.assert_called_once_with(
189+
"GET",
190+
"/api/v2/run-events/re-read-123",
191+
params={},
192+
)
193+
194+
# Verify result
195+
assert isinstance(result, RunEvent)
196+
assert result.id == "re-read-123"
197+
assert result.action == "planned"
198+
assert result.description == "Run planned successfully"
199+
200+
def test_read_run_event_with_includes(self, run_events_service):
201+
"""Test read with include options."""
202+
203+
mock_response_data = {
204+
"data": {
205+
"id": "re-read-456",
206+
"attributes": {
207+
"action": "discarded",
208+
"description": "Run discarded",
209+
"created-at": "2023-01-01T13:05:00Z",
210+
},
211+
}
212+
}
213+
214+
mock_response = Mock()
215+
mock_response.json.return_value = mock_response_data
216+
217+
with patch.object(run_events_service, "t") as mock_transport:
218+
mock_transport.request.return_value = mock_response
219+
220+
options = RunEventReadOptions(include=[RunEventIncludeOpt.RUN_EVENT_ACTOR])
221+
result = run_events_service.read_with_options("re-read-456", options)
222+
223+
# Verify include parameter was passed
224+
mock_transport.request.assert_called_once_with(
225+
"GET",
226+
"/api/v2/run-events/re-read-456",
227+
params={"include": "actor"},
228+
)
229+
230+
assert result.id == "re-read-456"
231+
assert result.action == "discarded"
232+
233+
def test_read_run_event_invalid_id(self, run_events_service):
234+
"""Test read with invalid run event ID."""
235+
236+
with pytest.raises(InvalidRunEventIDError):
237+
run_events_service.read("")
238+
239+
with pytest.raises(InvalidRunEventIDError):
240+
run_events_service.read("re/invalid")
241+
242+
def test_read_vs_read_with_options(self, run_events_service):
243+
"""Test that read() delegates to read_with_options()."""
244+
245+
mock_response_data = {
246+
"data": {
247+
"id": "re-read-789",
248+
"attributes": {
249+
"action": "completed",
250+
"created-at": "2023-01-01T13:10:00Z",
251+
},
252+
}
253+
}
254+
255+
mock_response = Mock()
256+
mock_response.json.return_value = mock_response_data
257+
258+
with patch.object(run_events_service, "t") as mock_transport:
259+
mock_transport.request.return_value = mock_response
260+
261+
result1 = run_events_service.read("re-read-789")
262+
263+
# Reset mock
264+
mock_transport.reset_mock()
265+
mock_transport.request.return_value = mock_response
266+
267+
result2 = run_events_service.read_with_options("re-read-789")
268+
269+
# Both should produce the same result
270+
assert result1.id == result2.id
271+
assert result1.action == result2.action
272+
273+
def test_list_run_events_iterator_lazy_loading(self, run_events_service):
274+
"""Test that list returns an iterator that lazily loads data."""
275+
276+
mock_data = [
277+
{
278+
"id": "re-lazy-1",
279+
"attributes": {
280+
"action": "queued",
281+
"created-at": "2023-01-01T12:00:00Z",
282+
},
283+
},
284+
]
285+
286+
with patch.object(run_events_service, "_list") as mock_list:
287+
mock_list.return_value = iter(mock_data)
288+
289+
# Get the iterator without consuming it yet
290+
iterator = run_events_service.list("run-lazy")
291+
292+
# _list should not have been called yet (iterator not consumed)
293+
# This test ensures lazy evaluation
294+
first_event = next(iterator)
295+
296+
# Now _list should have been called
297+
mock_list.assert_called_once()
298+
assert first_event.id == "re-lazy-1"

0 commit comments

Comments
 (0)