Skip to content

Commit 34e1704

Browse files
committed
update runs integration test
1 parent 70f2cc1 commit 34e1704

4 files changed

Lines changed: 297 additions & 10 deletions

File tree

python/lib/sift_client/_tests/resources/test_runs.py

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@
88
"""
99

1010
import os
11+
from datetime import datetime, timedelta, timezone
1112

1213
import pytest
1314

1415
from sift_client import SiftClient
1516
from sift_client.resources import RunsAPI, RunsAPIAsync
1617
from sift_client.sift_types import Run
18+
from sift_client.sift_types.run import RunCreate, RunUpdate
1719

1820
pytestmark = pytest.mark.integration
1921

@@ -58,6 +60,20 @@ def test_run(runs_api_sync):
5860
assert len(runs) >= 1
5961
return runs[0]
6062

63+
@pytest.fixture(scope="function")
64+
def new_run(runs_api_sync):
65+
"""Create a test run for update tests."""
66+
run_name = f"test_run_update_{datetime.now(timezone.utc).isoformat()}"
67+
description = "Test run created by Sift Client pytest"
68+
created_run = runs_api_sync.create(
69+
RunCreate(
70+
name=run_name,
71+
description=description,
72+
tags=["sift-client-pytest"],
73+
)
74+
)
75+
return created_run
76+
6177

6278
class TestRunsAPIAsync:
6379
"""Test suite for the async Runs API functionality."""
@@ -111,6 +127,8 @@ async def test_list_with_name_contains_filter(self, runs_api_async):
111127
for run in runs:
112128
assert "test" in run.name.lower()
113129

130+
# TODO: test run-specific filters
131+
114132
@pytest.mark.asyncio
115133
async def test_list_with_limit(self, runs_api_async):
116134
"""Test run listing with different limits."""
@@ -195,6 +213,281 @@ async def test_find_multiple_raises_error(self, runs_api_async):
195213
with pytest.raises(ValueError, match="Multiple"):
196214
await runs_api_async.find(name_contains="a")
197215

216+
class TestCreate:
217+
"""Tests for the async create method."""
218+
219+
@pytest.mark.asyncio
220+
async def test_create_basic_run(self, runs_api_async):
221+
"""Test creating a basic run with minimal fields."""
222+
run_name = f"test_run_create_{datetime.now(timezone.utc).isoformat()}"
223+
description = "Test run created by Sift Client pytest"
224+
run_create = RunCreate(
225+
name=run_name,
226+
description=description,
227+
tags=["sift-client-pytest"],
228+
)
229+
230+
created_run = await runs_api_async.create(run_create)
231+
232+
try:
233+
# Verify the run was created
234+
assert created_run is not None
235+
assert isinstance(created_run, Run)
236+
assert created_run.id_ is not None
237+
assert created_run.name == run_name
238+
assert created_run.description == description
239+
assert created_run.created_date is not None
240+
assert created_run.modified_date is not None
241+
finally:
242+
# Clean up: archive the test run
243+
await runs_api_async.archive(created_run)
244+
245+
@pytest.mark.asyncio
246+
async def test_create_run_with_all_fields(self, runs_api_async):
247+
"""Test creating a run with all optional fields."""
248+
run_name = f"test_run_full_{datetime.now(timezone.utc).isoformat()}"
249+
description = "Test run created by Sift Client pytest"
250+
start_time = datetime.now(timezone.utc) - timedelta(hours=1)
251+
stop_time = datetime.now(timezone.utc)
252+
253+
run_create = RunCreate(
254+
name=run_name,
255+
description=description,
256+
client_key=f"client_key_{datetime.now(timezone.utc).timestamp()}",
257+
start_time=start_time,
258+
stop_time=stop_time,
259+
tags=["test", "pytest", "integration", "sift-client-pytest"],
260+
metadata={"test_type": "integration", "version": "1.0", "is_automated": True},
261+
)
262+
263+
created_run = await runs_api_async.create(run_create)
264+
265+
try:
266+
# Verify all fields
267+
assert created_run.name == run_name
268+
assert created_run.description == description
269+
assert created_run.client_key is not None
270+
assert created_run.start_time is not None
271+
assert created_run.stop_time is not None
272+
assert created_run.tags == ["test", "pytest", "integration", "sift-client-pytest"]
273+
assert created_run.metadata["test_type"] == "integration"
274+
assert created_run.metadata["version"] == "1.0"
275+
assert created_run.metadata["is_automated"] is True
276+
finally:
277+
# Clean up
278+
await runs_api_async.archive(created_run)
279+
280+
@pytest.mark.asyncio
281+
async def test_create_run_with_dict(self, runs_api_async):
282+
"""Test creating a run using a dictionary instead of RunCreate object."""
283+
run_name = f"test_run_dict_{datetime.now(timezone.utc).isoformat()}"
284+
description = "Test run created by Sift Client pytest"
285+
286+
run_dict = {
287+
"name": run_name,
288+
"description": description,
289+
"tags": ["sift-client-pytest"],
290+
}
291+
292+
created_run = await runs_api_async.create(run_dict)
293+
294+
try:
295+
assert created_run.name == run_name
296+
assert created_run.description == description
297+
assert created_run.tags == ["sift-client-pytest"]
298+
finally:
299+
await runs_api_async.archive(created_run)
300+
301+
class TestUpdate:
302+
"""Tests for the async update method."""
303+
304+
@pytest.mark.asyncio
305+
async def test_update_run_description(self, runs_api_async, new_run):
306+
"""Test updating a run's description."""
307+
try:
308+
# Update the description
309+
update = RunUpdate(description="Updated description")
310+
updated_run = await runs_api_async.update(new_run, update)
311+
312+
# Verify the update
313+
assert updated_run.id_ == new_run.id_
314+
assert updated_run.description == "Updated description"
315+
assert updated_run.name == new_run.name # Name should remain unchanged
316+
finally:
317+
await runs_api_async.archive(new_run.id_)
318+
319+
@pytest.mark.asyncio
320+
async def test_update_run_name(self, runs_api_async, new_run):
321+
"""Test updating a run's name."""
322+
try:
323+
# Update the name
324+
new_name = f"updated_{new_run.name}"
325+
update = RunUpdate(name=new_name)
326+
updated_run = await runs_api_async.update(new_run, update)
327+
328+
# Verify the update
329+
assert updated_run.name == new_name
330+
assert updated_run.id_ == new_run.id_
331+
finally:
332+
await runs_api_async.archive(new_run.id_)
333+
334+
@pytest.mark.asyncio
335+
async def test_update_run_tags_and_metadata(self, runs_api_async, new_run):
336+
"""Test updating a run's tags and metadata."""
337+
try:
338+
# Update tags
339+
update = RunUpdate(
340+
tags=["updated", "new-tag", "sift-client-pytest"],
341+
)
342+
updated_run = await runs_api_async.update(new_run, update)
343+
344+
# Verify the updates
345+
assert set(updated_run.tags) == {"updated", "new-tag", "sift-client-pytest"}
346+
finally:
347+
await runs_api_async.archive(new_run.id_)
348+
349+
@pytest.mark.asyncio
350+
async def test_update_run_times(self, runs_api_async, new_run):
351+
"""Test updating a run's start and stop times."""
352+
try:
353+
# Update with start and stop times
354+
start_time = datetime.now(timezone.utc) - timedelta(hours=2)
355+
stop_time = datetime.now(timezone.utc) - timedelta(hours=1)
356+
update = RunUpdate(start_time=start_time, stop_time=stop_time)
357+
updated_run = await runs_api_async.update(new_run, update)
358+
359+
# Verify the times were set
360+
assert updated_run.start_time is not None
361+
assert updated_run.stop_time is not None
362+
# Allow for small time differences due to serialization
363+
assert abs((updated_run.start_time - start_time).total_seconds()) < 1
364+
assert abs((updated_run.stop_time - stop_time).total_seconds()) < 1
365+
finally:
366+
await runs_api_async.archive(new_run.id_)
367+
368+
@pytest.mark.asyncio
369+
async def test_update_with_dict(self, runs_api_async, new_run):
370+
"""Test updating a run using a dictionary instead of RunUpdate object."""
371+
try:
372+
# Update using dict
373+
update_dict = {"description": "Updated via dict"}
374+
updated_run = await runs_api_async.update(new_run, update_dict)
375+
376+
assert updated_run.description == "Updated via dict"
377+
finally:
378+
await runs_api_async.archive(new_run.id_)
379+
380+
@pytest.mark.asyncio
381+
async def test_update_with_run_id_string(self, runs_api_async, new_run):
382+
"""Test updating a run by passing run ID as string."""
383+
try:
384+
# Update using run ID string
385+
update = RunUpdate(description="Updated via ID string")
386+
updated_run = await runs_api_async.update(new_run.id_, update)
387+
388+
assert updated_run.id_ == new_run.id_
389+
assert updated_run.description == "Updated via ID string"
390+
finally:
391+
await runs_api_async.archive(new_run.id_)
392+
393+
class TestArchive:
394+
"""Tests for the async archive method."""
395+
396+
@pytest.mark.asyncio
397+
async def test_archive_run(self, runs_api_async, new_run):
398+
"""Test archiving a run."""
399+
run = await runs_api_async.archive(new_run)
400+
401+
assert isinstance(run, Run)
402+
assert run.id_ == new_run.id_
403+
assert run.is_archived is True
404+
405+
# Verify it's archived by checking it doesn't appear in normal list
406+
runs_without_archived = await runs_api_async.list_(
407+
name=new_run.name, include_archived=False
408+
)
409+
assert len(runs_without_archived) == 0
410+
411+
# Verify it appears when including archived
412+
runs_with_archived = await runs_api_async.list_(
413+
name=new_run.name, include_archived=True
414+
)
415+
assert len(runs_with_archived) == 1
416+
assert runs_with_archived[0].id_ == new_run.id_
417+
assert runs_with_archived[0].archived_date is not None
418+
419+
@pytest.mark.asyncio
420+
async def test_archive_with_run_id_string(self, runs_api_async, new_run):
421+
"""Test archiving a run by passing run ID as string."""
422+
# Archive using run ID string
423+
run = await runs_api_async.archive(new_run.id_)
424+
425+
assert isinstance(run, Run)
426+
assert run.id_ == new_run.id_
427+
assert run.is_archived is True
428+
429+
@pytest.mark.asyncio
430+
async def test_get_archived_run_by_id(self, runs_api_async, new_run):
431+
"""Test that we can still get an archived run by ID."""
432+
# Archive the test run
433+
run = await runs_api_async.archive(new_run)
434+
435+
assert isinstance(run, Run)
436+
assert run.id_ == new_run.id_
437+
assert run.is_archived is True
438+
439+
class TestStop:
440+
"""Tests for the async stop method."""
441+
442+
@pytest.mark.asyncio
443+
async def test_stop_run(self, runs_api_async, new_run):
444+
"""Test stopping a run."""
445+
try:
446+
# Stop the run
447+
stopped_run = await runs_api_async.stop(new_run)
448+
449+
# Verify the run was stopped
450+
assert isinstance(stopped_run, Run)
451+
assert stopped_run.id_ == new_run.id_
452+
assert stopped_run.stop_time is not None
453+
finally:
454+
await runs_api_async.archive(new_run.id_)
455+
456+
@pytest.mark.asyncio
457+
async def test_stop_run_with_id_string(self, runs_api_async, new_run):
458+
"""Test stopping a run by passing run ID as string."""
459+
try:
460+
# Stop using run ID string
461+
stopped_run = await runs_api_async.stop(new_run.id_)
462+
463+
# Verify the run was stopped
464+
assert isinstance(stopped_run, Run)
465+
assert stopped_run.id_ == new_run.id_
466+
assert stopped_run.stop_time is not None
467+
finally:
468+
await runs_api_async.archive(new_run.id_)
469+
470+
@pytest.mark.asyncio
471+
async def test_stop_run_with_start_time(self, runs_api_async, new_run):
472+
"""Test stopping a run that has a start time."""
473+
try:
474+
# Set start time first
475+
start_time = datetime.now(timezone.utc) - timedelta(hours=1)
476+
update = RunUpdate(start_time=start_time)
477+
await runs_api_async.update(new_run, update)
478+
479+
# Stop the run
480+
stopped_run = await runs_api_async.stop(new_run)
481+
482+
# Verify the run was stopped and times are valid
483+
assert stopped_run.stop_time is not None
484+
assert stopped_run.start_time is not None
485+
assert stopped_run.stop_time > stopped_run.start_time
486+
finally:
487+
await runs_api_async.archive(new_run.id_)
488+
489+
490+
198491

199492
class TestRunsAPISync:
200493
"""Test suite for the synchronous Runs API functionality.

python/lib/sift_client/resources/runs.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,14 +244,15 @@ async def archive(
244244
async def stop(
245245
self,
246246
run: str | Run,
247-
) -> None:
247+
) -> Run:
248248
"""Stop a run by setting its stop time to the current time.
249249
250250
Args:
251251
run: The Run or run ID to stop.
252252
"""
253253
run_id = run._id_or_error if isinstance(run, Run) else run
254254
await self._low_level_client.stop_run(run_id=run_id or "")
255+
return await self.get(run_id=run_id)
255256

256257
async def create_automatic_association_for_assets(
257258
self,

python/lib/sift_client/sift_types/_base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ def _build_proto_and_paths(
151151
try:
152152
setattr(proto_msg, field_name, value)
153153
paths.append(path)
154-
except TypeError as e:
154+
except (TypeError, AttributeError) as e:
155155
raise TypeError(
156156
f"Can't set {field_name} to {value} on {proto_msg.__class__.__name__}"
157157
) from e
@@ -174,7 +174,7 @@ def to_proto(self) -> ProtoT:
174174
proto_msg = proto_cls()
175175

176176
# Get all fields
177-
data = self.model_dump(exclude_none=False)
177+
data = self.model_dump(exclude_unset=True)
178178
self._build_proto_and_paths(proto_msg, data)
179179

180180
return proto_msg

python/lib/sift_client/sift_types/run.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -155,13 +155,6 @@ class RunUpdate(RunBase, ModelUpdate[RunProto]):
155155

156156
name: str | None = None
157157

158-
@model_validator(mode="after")
159-
def _validate_non_updatable_fields(self):
160-
"""Validate that the fields that cannot be updated are not set."""
161-
if self.client_key is not None:
162-
raise ValueError("Cannot update client key")
163-
return self
164-
165158
def _get_proto_class(self) -> type[RunProto]:
166159
return RunProto
167160

0 commit comments

Comments
 (0)