Skip to content

Commit 68015de

Browse files
authored
python(feat): Add FileAttachmentsMixin to TestStep. (#466)
1 parent ba3a3c7 commit 68015de

6 files changed

Lines changed: 81 additions & 42 deletions

File tree

python/lib/sift_client/_tests/sift_types/test_results.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
from __future__ import annotations
44

5+
import tempfile
56
from datetime import datetime, timedelta, timezone
6-
from unittest.mock import MagicMock
7+
from unittest.mock import MagicMock, call
78

89
import pytest
910

@@ -230,3 +231,41 @@ def test_attachments_property_fetches_files(self, mock_test_report, mock_client)
230231

231232
# Verify result
232233
assert result == mock_remote_files
234+
235+
def test_upload_attachment(self, mock_test_report, mock_test_step, mock_client):
236+
"""Ensure test report and step have FileAttachmentsMixin and it is called correctly."""
237+
# Create mock file attachment to be returned
238+
mock_file_attachment = MagicMock()
239+
mock_file_attachment.description = "Test upload to test report"
240+
mock_file_attachment.entity_id = mock_test_report.id_
241+
mock_client.file_attachments.upload.return_value = mock_file_attachment
242+
243+
# Create a temporary test file
244+
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as tmp:
245+
tmp.write("Test file content\n")
246+
tmp_path = tmp.name
247+
248+
_ = mock_test_report.upload_attachment(
249+
path=tmp_path, description="Test upload to test report"
250+
)
251+
_ = mock_test_step.upload_attachment(path=tmp_path, description="Test upload to test step")
252+
253+
# Verify file_attachments.upload was called with correct parameters
254+
mock_client.file_attachments.upload.assert_has_calls(
255+
[
256+
call(
257+
path=tmp_path,
258+
entity=mock_test_report,
259+
metadata=None,
260+
description="Test upload to test report",
261+
organization_id=None,
262+
),
263+
call(
264+
path=tmp_path,
265+
entity=mock_test_step,
266+
metadata=None,
267+
description="Test upload to test step",
268+
organization_id=None,
269+
),
270+
]
271+
)

python/lib/sift_client/resources/file_attachments.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
RemoteFileEntityType,
2020
)
2121
from sift_client.sift_types.run import Run
22-
from sift_client.sift_types.test_report import TestReport
22+
from sift_client.sift_types.test_report import TestReport, TestStep
2323

2424

2525
class FileAttachmentsAPIAsync(ResourceBase):
@@ -94,7 +94,7 @@ async def list_(
9494
# metadata TODO: Add to backend
9595
# metadata: list[Any] | None = None,
9696
# file specific
97-
entities: list[Run | Asset | TestReport] | None = None,
97+
entities: list[Run | Asset | TestReport | TestStep] | None = None,
9898
entity_type: RemoteFileEntityType | None = None,
9999
entity_ids: list[str] | None = None,
100100
# common filters
@@ -255,7 +255,7 @@ async def upload(
255255
self,
256256
*,
257257
path: str | Path,
258-
entity: Asset | Run | TestReport,
258+
entity: Asset | Run | TestReport | TestStep,
259259
metadata: dict[str, Any] | None = None,
260260
description: str | None = None,
261261
organization_id: str | None = None,

python/lib/sift_client/resources/sync_stubs/__init__.pyi

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -582,7 +582,7 @@ class FileAttachmentsAPI:
582582
name_contains: str | None = None,
583583
name_regex: str | re.Pattern | None = None,
584584
remote_file_ids: list[str] | None = None,
585-
entities: list[Run | Asset | TestReport] | None = None,
585+
entities: list[Run | Asset | TestReport | TestStep] | None = None,
586586
entity_type: RemoteFileEntityType | None = None,
587587
entity_ids: list[str] | None = None,
588588
description_contains: str | None = None,
@@ -626,7 +626,7 @@ class FileAttachmentsAPI:
626626
self,
627627
*,
628628
path: str | Path,
629-
entity: Asset | Run | TestReport,
629+
entity: Asset | Run | TestReport | TestStep,
630630
metadata: dict[str, Any] | None = None,
631631
description: str | None = None,
632632
organization_id: str | None = None,

python/lib/sift_client/sift_types/_base.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
Callable,
1010
ClassVar,
1111
Generic,
12+
Protocol,
1213
TypeVar,
1314
)
1415

@@ -23,6 +24,19 @@
2324
SelfT = TypeVar("SelfT", bound="BaseType")
2425

2526

27+
class BaseTypeProtocol(Protocol):
28+
"""Protocol for defining public properties for types that inherit from BaseType."""
29+
30+
@property
31+
def client(self) -> SiftClient: ...
32+
33+
@property
34+
def id_(self) -> str | None: ...
35+
36+
@property
37+
def _id_or_error(self) -> str: ...
38+
39+
2640
class BaseType(BaseModel, Generic[ProtoT, SelfT], ABC):
2741
model_config = ConfigDict(frozen=True)
2842

python/lib/sift_client/sift_types/_mixins/file_attachments.py

Lines changed: 21 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,40 @@
11
from __future__ import annotations
22

3-
from typing import TYPE_CHECKING, Any, ClassVar, Protocol
3+
from typing import TYPE_CHECKING, Any, ClassVar
44

55
if TYPE_CHECKING:
66
from pathlib import Path
77

8-
from sift_client.client import SiftClient
8+
from sift_client.sift_types._base import BaseTypeProtocol
99
from sift_client.sift_types.file_attachment import FileAttachment
1010

1111

12-
class _SupportsFileAttachments(Protocol):
13-
"""Protocol for types that support file attachments."""
14-
15-
@property
16-
def client(self) -> SiftClient: ...
17-
18-
@property
19-
def id_(self) -> str | None: ...
20-
21-
2212
class FileAttachmentsMixin:
2313
"""Mixin for sift_types that support file attachments (remote files).
2414
2515
This mixin assumes the class also inherits from BaseType, which provides:
2616
- id_: str | None
2717
- client: SiftClient property
28-
29-
The entity type is automatically determined from the class name:
30-
- Asset -> assets
31-
- Run -> runs
32-
- TestReport -> test_reports
3318
"""
3419

3520
# Mapping of class names to entity types (REST API format)
3621
_ENTITY_TYPE_MAP: ClassVar[dict[str, str]] = {
3722
"Asset": "assets",
3823
"Run": "runs",
3924
"TestReport": "test_reports",
25+
"TestStep": "test_steps",
4026
}
4127

28+
@staticmethod
29+
def check_is_supported_entity_type(cls):
30+
"""Check if the entity type is supported for file attachments.
31+
32+
Returns:
33+
True if the entity type is supported, False otherwise.
34+
"""
35+
if not cls.__class__.__name__ in FileAttachmentsMixin._ENTITY_TYPE_MAP:
36+
raise ValueError(f"{cls.__name__} does not support file attachments")
37+
4238
def _get_entity_type_name(self) -> str:
4339
"""Get the entity type string.
4440
@@ -49,7 +45,7 @@ def _get_entity_type_name(self) -> str:
4945
ValueError: If the class name is not in the entity type mapping.
5046
"""
5147
class_name = self.__class__.__name__
52-
entity_type = self._ENTITY_TYPE_MAP.get(class_name)
48+
entity_type = FileAttachmentsMixin._ENTITY_TYPE_MAP.get(self.__class__.__name__)
5349

5450
if not entity_type:
5551
raise ValueError(
@@ -60,24 +56,19 @@ def _get_entity_type_name(self) -> str:
6056
return entity_type
6157

6258
@property
63-
def attachments(self: _SupportsFileAttachments) -> list[FileAttachment]:
59+
def attachments(self: BaseTypeProtocol) -> list[FileAttachment]:
6460
"""Get all file attachments for this entity.
6561
6662
Returns:
6763
A list of FileAttachments associated with this entity.
6864
"""
69-
from sift_client.sift_types.asset import Asset
70-
from sift_client.sift_types.run import Run
71-
from sift_client.sift_types.test_report import TestReport
72-
73-
if not isinstance(self, (Asset, Run, TestReport)):
74-
raise ValueError("Entity is not a valid entity type")
65+
FileAttachmentsMixin.check_is_supported_entity_type(self)
7566
return self.client.file_attachments.list_(
76-
entities=[self],
67+
entities=[self], # type: ignore
7768
)
7869

7970
def delete_attachment(
80-
self: _SupportsFileAttachments,
71+
self: BaseTypeProtocol,
8172
file_attachment: list[FileAttachment | str] | FileAttachment | str,
8273
) -> None:
8374
"""Delete one or more file attachments.
@@ -88,7 +79,7 @@ def delete_attachment(
8879
self.client.file_attachments.delete(file_attachments=file_attachment)
8980

9081
def upload_attachment(
91-
self: _SupportsFileAttachments,
82+
self: BaseTypeProtocol,
9283
path: str | Path,
9384
metadata: dict[str, Any] | None = None,
9485
description: str | None = None,
@@ -105,15 +96,10 @@ def upload_attachment(
10596
Returns:
10697
The uploaded FileAttachment.
10798
"""
108-
from sift_client.sift_types.asset import Asset
109-
from sift_client.sift_types.run import Run
110-
from sift_client.sift_types.test_report import TestReport
111-
112-
if not isinstance(self, (Asset, Run, TestReport)):
113-
raise ValueError("Entity is not a valid entity type")
99+
FileAttachmentsMixin.check_is_supported_entity_type(self)
114100
return self.client.file_attachments.upload(
115101
path=path,
116-
entity=self,
102+
entity=self, # type: ignore
117103
metadata=metadata,
118104
description=description,
119105
organization_id=organization_id,

python/lib/sift_client/sift_types/test_report.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ def to_proto(self) -> TestStepProto:
136136
return proto
137137

138138

139-
class TestStep(BaseType[TestStepProto, "TestStep"]):
139+
class TestStep(BaseType[TestStepProto, "TestStep"], FileAttachmentsMixin):
140140
"""TestStep model representing a step in a test."""
141141

142142
test_report_id: str

0 commit comments

Comments
 (0)