Skip to content

Commit c79c77f

Browse files
authored
CUST-5037 Fix attachment id to not be a requirement (#449)
1 parent 6eddb61 commit c79c77f

File tree

3 files changed

+239
-4
lines changed

3 files changed

+239
-4
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
nylas-python Changelog
22
======================
33

4+
Unrelease
5+
----------
6+
* Update attachment schema to not make it mandatory
7+
48
v6.14.0
59
----------
610
* Added `message.deleted` to the Webhook enum, appended tests

nylas/models/attachments.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class Attachment:
2222
is_inline: Whether the attachment is inline.
2323
"""
2424

25-
id: str
25+
id: Optional[str] = None
2626
grant_id: Optional[str] = None
2727
filename: Optional[str] = None
2828
content_type: Optional[str] = None

tests/resources/test_attachments.py

Lines changed: 234 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
from io import BytesIO
12
from unittest.mock import Mock
23

3-
from nylas.models.attachments import Attachment, FindAttachmentQueryParams
4+
from nylas.models.attachments import Attachment, CreateAttachmentRequest, FindAttachmentQueryParams
45
from nylas.resources.attachments import Attachments
56

67

7-
class TestAttachments:
8-
def test_attachment_deserialization(self, http_client):
8+
class TestAttachmentModel:
9+
"""Tests for the Attachment dataclass model."""
10+
11+
def test_attachment_deserialization(self):
12+
"""Test full deserialization of Attachment from dict."""
913
attach_json = {
1014
"content_type": "image/png",
1115
"filename": "pic.png",
@@ -14,6 +18,7 @@ def test_attachment_deserialization(self, http_client):
1418
"is_inline": True,
1519
"size": 13068,
1620
"content_id": "<ce9b9547-9eeb-43b2-ac4e-58768bdf04e4>",
21+
"content_disposition": "inline",
1722
}
1823

1924
attachment = Attachment.from_dict(attach_json)
@@ -25,6 +30,232 @@ def test_attachment_deserialization(self, http_client):
2530
assert attachment.is_inline is True
2631
assert attachment.size == 13068
2732
assert attachment.content_id == "<ce9b9547-9eeb-43b2-ac4e-58768bdf04e4>"
33+
assert attachment.content_disposition == "inline"
34+
35+
def test_attachment_serialization(self):
36+
"""Test serialization of Attachment to dict."""
37+
attachment = Attachment(
38+
id="185e56cb50e12e82",
39+
grant_id="41009df5-bf11-4c97-aa18-b285b5f2e386",
40+
filename="document.pdf",
41+
content_type="application/pdf",
42+
size=2048,
43+
content_id="<doc-123>",
44+
content_disposition="attachment",
45+
is_inline=False,
46+
)
47+
48+
result = attachment.to_dict()
49+
50+
assert result["id"] == "185e56cb50e12e82"
51+
assert result["grant_id"] == "41009df5-bf11-4c97-aa18-b285b5f2e386"
52+
assert result["filename"] == "document.pdf"
53+
assert result["content_type"] == "application/pdf"
54+
assert result["size"] == 2048
55+
assert result["content_id"] == "<doc-123>"
56+
assert result["content_disposition"] == "attachment"
57+
assert result["is_inline"] is False
58+
59+
def test_attachment_deserialization_partial_fields(self):
60+
"""Test deserialization with only required fields."""
61+
attach_json = {
62+
"id": "abc123",
63+
"filename": "test.txt",
64+
}
65+
66+
attachment = Attachment.from_dict(attach_json)
67+
68+
assert attachment.id == "abc123"
69+
assert attachment.filename == "test.txt"
70+
assert attachment.grant_id is None
71+
assert attachment.content_type is None
72+
assert attachment.size is None
73+
assert attachment.content_id is None
74+
assert attachment.content_disposition is None
75+
assert attachment.is_inline is None
76+
77+
def test_attachment_deserialization_empty_dict(self):
78+
"""Test deserialization from empty dict."""
79+
attachment = Attachment.from_dict({})
80+
81+
assert attachment.id is None
82+
assert attachment.grant_id is None
83+
assert attachment.filename is None
84+
assert attachment.content_type is None
85+
assert attachment.size is None
86+
assert attachment.content_id is None
87+
assert attachment.content_disposition is None
88+
assert attachment.is_inline is None
89+
90+
def test_attachment_default_values(self):
91+
"""Test Attachment instantiation with default values."""
92+
attachment = Attachment()
93+
94+
assert attachment.id is None
95+
assert attachment.grant_id is None
96+
assert attachment.filename is None
97+
assert attachment.content_type is None
98+
assert attachment.size is None
99+
assert attachment.content_id is None
100+
assert attachment.content_disposition is None
101+
assert attachment.is_inline is None
102+
103+
def test_attachment_content_disposition_attachment(self):
104+
"""Test attachment with content_disposition set to 'attachment'."""
105+
attach_json = {
106+
"id": "file-123",
107+
"filename": "report.xlsx",
108+
"content_disposition": "attachment",
109+
"is_inline": False,
110+
}
111+
112+
attachment = Attachment.from_dict(attach_json)
113+
114+
assert attachment.content_disposition == "attachment"
115+
assert attachment.is_inline is False
116+
117+
def test_attachment_content_disposition_inline(self):
118+
"""Test inline attachment with content_disposition."""
119+
attach_json = {
120+
"id": "img-456",
121+
"filename": "logo.png",
122+
"content_disposition": "inline",
123+
"is_inline": True,
124+
"content_id": "<[email protected]>",
125+
}
126+
127+
attachment = Attachment.from_dict(attach_json)
128+
129+
assert attachment.content_disposition == "inline"
130+
assert attachment.is_inline is True
131+
assert attachment.content_id == "<[email protected]>"
132+
133+
def test_attachment_roundtrip_serialization(self):
134+
"""Test that serialization and deserialization are inverses."""
135+
original = Attachment(
136+
id="test-id",
137+
grant_id="grant-123",
138+
filename="file.txt",
139+
content_type="text/plain",
140+
size=100,
141+
content_id="<cid123>",
142+
content_disposition="attachment",
143+
is_inline=False,
144+
)
145+
146+
serialized = original.to_dict()
147+
deserialized = Attachment.from_dict(serialized)
148+
149+
assert deserialized.id == original.id
150+
assert deserialized.grant_id == original.grant_id
151+
assert deserialized.filename == original.filename
152+
assert deserialized.content_type == original.content_type
153+
assert deserialized.size == original.size
154+
assert deserialized.content_id == original.content_id
155+
assert deserialized.content_disposition == original.content_disposition
156+
assert deserialized.is_inline == original.is_inline
157+
158+
159+
class TestCreateAttachmentRequest:
160+
"""Tests for the CreateAttachmentRequest TypedDict."""
161+
162+
def test_create_attachment_request_with_base64_content(self):
163+
"""Test creating attachment request with base64 encoded content."""
164+
request: CreateAttachmentRequest = {
165+
"filename": "test.txt",
166+
"content_type": "text/plain",
167+
"content": "SGVsbG8gV29ybGQh", # base64 for "Hello World!"
168+
"size": 12,
169+
}
170+
171+
assert request["filename"] == "test.txt"
172+
assert request["content_type"] == "text/plain"
173+
assert request["content"] == "SGVsbG8gV29ybGQh"
174+
assert request["size"] == 12
175+
176+
def test_create_attachment_request_with_file_object(self):
177+
"""Test creating attachment request with file-like object."""
178+
file_content = BytesIO(b"File content here")
179+
180+
request: CreateAttachmentRequest = {
181+
"filename": "document.pdf",
182+
"content_type": "application/pdf",
183+
"content": file_content,
184+
"size": 17,
185+
}
186+
187+
assert request["filename"] == "document.pdf"
188+
assert request["content_type"] == "application/pdf"
189+
assert request["content"] == file_content
190+
assert request["size"] == 17
191+
192+
def test_create_attachment_request_with_optional_fields(self):
193+
"""Test creating attachment request with all optional fields."""
194+
request: CreateAttachmentRequest = {
195+
"filename": "image.png",
196+
"content_type": "image/png",
197+
"content": "iVBORw0KGgo=",
198+
"size": 1024,
199+
"content_id": "<[email protected]>",
200+
"content_disposition": "inline",
201+
"is_inline": True,
202+
}
203+
204+
assert request["filename"] == "image.png"
205+
assert request["content_type"] == "image/png"
206+
assert request["content"] == "iVBORw0KGgo="
207+
assert request["size"] == 1024
208+
assert request["content_id"] == "<[email protected]>"
209+
assert request["content_disposition"] == "inline"
210+
assert request["is_inline"] is True
211+
212+
def test_create_attachment_request_minimal(self):
213+
"""Test creating attachment request with only required fields."""
214+
request: CreateAttachmentRequest = {
215+
"filename": "minimal.txt",
216+
"content_type": "text/plain",
217+
"content": "data",
218+
"size": 4,
219+
}
220+
221+
assert "filename" in request
222+
assert "content_type" in request
223+
assert "content" in request
224+
assert "size" in request
225+
# Optional fields should not be present
226+
assert "content_id" not in request
227+
assert "content_disposition" not in request
228+
assert "is_inline" not in request
229+
230+
231+
class TestFindAttachmentQueryParams:
232+
"""Tests for the FindAttachmentQueryParams TypedDict."""
233+
234+
def test_find_attachment_query_params(self):
235+
"""Test creating find attachment query params."""
236+
params: FindAttachmentQueryParams = {
237+
"message_id": "msg-12345",
238+
}
239+
240+
assert params["message_id"] == "msg-12345"
241+
242+
def test_find_attachment_query_params_various_message_ids(self):
243+
"""Test find attachment query params with various message ID formats."""
244+
# Simple ID
245+
params1: FindAttachmentQueryParams = {"message_id": "abc123"}
246+
assert params1["message_id"] == "abc123"
247+
248+
# UUID format
249+
params2: FindAttachmentQueryParams = {"message_id": "550e8400-e29b-41d4-a716-446655440000"}
250+
assert params2["message_id"] == "550e8400-e29b-41d4-a716-446655440000"
251+
252+
# Complex message ID (email message-id format)
253+
params3: FindAttachmentQueryParams = {"message_id": "<[email protected]>"}
254+
assert params3["message_id"] == "<[email protected]>"
255+
256+
257+
class TestAttachments:
258+
"""Tests for the Attachments resource API calls."""
28259

29260
def test_find_attachment(self, http_client_response):
30261
attachments = Attachments(http_client_response)

0 commit comments

Comments
 (0)