jaf-py 2.5.9__py3-none-any.whl → 2.5.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jaf/__init__.py +154 -57
- jaf/a2a/__init__.py +42 -21
- jaf/a2a/agent.py +79 -126
- jaf/a2a/agent_card.py +87 -78
- jaf/a2a/client.py +30 -66
- jaf/a2a/examples/client_example.py +12 -12
- jaf/a2a/examples/integration_example.py +38 -47
- jaf/a2a/examples/server_example.py +56 -53
- jaf/a2a/memory/__init__.py +0 -4
- jaf/a2a/memory/cleanup.py +28 -21
- jaf/a2a/memory/factory.py +155 -133
- jaf/a2a/memory/providers/composite.py +21 -26
- jaf/a2a/memory/providers/in_memory.py +89 -83
- jaf/a2a/memory/providers/postgres.py +117 -115
- jaf/a2a/memory/providers/redis.py +128 -121
- jaf/a2a/memory/serialization.py +77 -87
- jaf/a2a/memory/tests/run_comprehensive_tests.py +112 -83
- jaf/a2a/memory/tests/test_cleanup.py +211 -94
- jaf/a2a/memory/tests/test_serialization.py +73 -68
- jaf/a2a/memory/tests/test_stress_concurrency.py +186 -133
- jaf/a2a/memory/tests/test_task_lifecycle.py +138 -120
- jaf/a2a/memory/types.py +91 -53
- jaf/a2a/protocol.py +95 -125
- jaf/a2a/server.py +90 -118
- jaf/a2a/standalone_client.py +30 -43
- jaf/a2a/tests/__init__.py +16 -33
- jaf/a2a/tests/run_tests.py +17 -53
- jaf/a2a/tests/test_agent.py +40 -140
- jaf/a2a/tests/test_client.py +54 -117
- jaf/a2a/tests/test_integration.py +28 -82
- jaf/a2a/tests/test_protocol.py +54 -139
- jaf/a2a/tests/test_types.py +50 -136
- jaf/a2a/types.py +58 -34
- jaf/cli.py +21 -41
- jaf/core/__init__.py +7 -1
- jaf/core/agent_tool.py +93 -72
- jaf/core/analytics.py +257 -207
- jaf/core/checkpoint.py +223 -0
- jaf/core/composition.py +249 -235
- jaf/core/engine.py +817 -519
- jaf/core/errors.py +55 -42
- jaf/core/guardrails.py +276 -202
- jaf/core/handoff.py +47 -31
- jaf/core/parallel_agents.py +69 -75
- jaf/core/performance.py +75 -73
- jaf/core/proxy.py +43 -44
- jaf/core/proxy_helpers.py +24 -27
- jaf/core/regeneration.py +220 -129
- jaf/core/state.py +68 -66
- jaf/core/streaming.py +115 -108
- jaf/core/tool_results.py +111 -101
- jaf/core/tools.py +114 -116
- jaf/core/tracing.py +269 -210
- jaf/core/types.py +371 -151
- jaf/core/workflows.py +209 -168
- jaf/exceptions.py +46 -38
- jaf/memory/__init__.py +1 -6
- jaf/memory/approval_storage.py +54 -77
- jaf/memory/factory.py +4 -4
- jaf/memory/providers/in_memory.py +216 -180
- jaf/memory/providers/postgres.py +216 -146
- jaf/memory/providers/redis.py +173 -116
- jaf/memory/types.py +70 -51
- jaf/memory/utils.py +36 -34
- jaf/plugins/__init__.py +12 -12
- jaf/plugins/base.py +105 -96
- jaf/policies/__init__.py +0 -1
- jaf/policies/handoff.py +37 -46
- jaf/policies/validation.py +76 -52
- jaf/providers/__init__.py +6 -3
- jaf/providers/mcp.py +97 -51
- jaf/providers/model.py +361 -280
- jaf/server/__init__.py +1 -1
- jaf/server/main.py +7 -11
- jaf/server/server.py +514 -359
- jaf/server/types.py +208 -52
- jaf/utils/__init__.py +17 -18
- jaf/utils/attachments.py +111 -116
- jaf/utils/document_processor.py +175 -174
- jaf/visualization/__init__.py +1 -1
- jaf/visualization/example.py +111 -110
- jaf/visualization/functional_core.py +46 -71
- jaf/visualization/graphviz.py +154 -189
- jaf/visualization/imperative_shell.py +7 -16
- jaf/visualization/types.py +8 -4
- {jaf_py-2.5.9.dist-info → jaf_py-2.5.11.dist-info}/METADATA +2 -2
- jaf_py-2.5.11.dist-info/RECORD +97 -0
- jaf_py-2.5.9.dist-info/RECORD +0 -96
- {jaf_py-2.5.9.dist-info → jaf_py-2.5.11.dist-info}/WHEEL +0 -0
- {jaf_py-2.5.9.dist-info → jaf_py-2.5.11.dist-info}/entry_points.txt +0 -0
- {jaf_py-2.5.9.dist-info → jaf_py-2.5.11.dist-info}/licenses/LICENSE +0 -0
- {jaf_py-2.5.9.dist-info → jaf_py-2.5.11.dist-info}/top_level.txt +0 -0
jaf/utils/attachments.py
CHANGED
|
@@ -15,7 +15,7 @@ from ..core.types import Attachment
|
|
|
15
15
|
|
|
16
16
|
class AttachmentValidationError(Exception):
|
|
17
17
|
"""Exception raised when attachment validation fails."""
|
|
18
|
-
|
|
18
|
+
|
|
19
19
|
def __init__(self, message: str, field: Optional[str] = None):
|
|
20
20
|
super().__init__(message)
|
|
21
21
|
self.field = field
|
|
@@ -28,14 +28,22 @@ BASE64_SIZE_RATIO = 0.75 # Base64 decoded size is approximately 3/4 of the enco
|
|
|
28
28
|
MAX_FORMAT_LENGTH = 10
|
|
29
29
|
|
|
30
30
|
ALLOWED_IMAGE_MIME_TYPES = [
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
"image/jpeg",
|
|
32
|
+
"image/jpg",
|
|
33
|
+
"image/png",
|
|
34
|
+
"image/gif",
|
|
35
|
+
"image/webp",
|
|
36
|
+
"image/bmp",
|
|
37
|
+
"image/svg+xml",
|
|
33
38
|
]
|
|
34
39
|
|
|
35
40
|
ALLOWED_DOCUMENT_MIME_TYPES = [
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
"application/pdf",
|
|
42
|
+
"text/plain",
|
|
43
|
+
"text/csv",
|
|
44
|
+
"application/json",
|
|
45
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
46
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
39
47
|
]
|
|
40
48
|
|
|
41
49
|
|
|
@@ -43,18 +51,18 @@ def _validate_base64(data: str) -> bool:
|
|
|
43
51
|
"""Validate base64 string format."""
|
|
44
52
|
try:
|
|
45
53
|
# Basic base64 pattern check
|
|
46
|
-
base64_pattern = re.compile(r
|
|
54
|
+
base64_pattern = re.compile(r"^[A-Za-z0-9+/]*={0,2}$")
|
|
47
55
|
if not base64_pattern.match(data):
|
|
48
56
|
return False
|
|
49
|
-
|
|
57
|
+
|
|
50
58
|
# Try to decode to verify it's valid base64
|
|
51
59
|
decoded = base64.b64decode(data)
|
|
52
|
-
reencoded = base64.b64encode(decoded).decode(
|
|
53
|
-
|
|
60
|
+
reencoded = base64.b64encode(decoded).decode("ascii")
|
|
61
|
+
|
|
54
62
|
# Account for padding differences
|
|
55
|
-
normalized_input = data.rstrip(
|
|
56
|
-
normalized_reencoded = reencoded.rstrip(
|
|
57
|
-
|
|
63
|
+
normalized_input = data.rstrip("=")
|
|
64
|
+
normalized_reencoded = reencoded.rstrip("=")
|
|
65
|
+
|
|
58
66
|
return normalized_input == normalized_reencoded
|
|
59
67
|
except Exception:
|
|
60
68
|
return False
|
|
@@ -65,17 +73,17 @@ def _validate_attachment_size(data: Optional[str]) -> None:
|
|
|
65
73
|
if data:
|
|
66
74
|
# Calculate exact decoded size for base64 data
|
|
67
75
|
# Remove padding to get accurate count
|
|
68
|
-
data_without_padding = data.rstrip(
|
|
76
|
+
data_without_padding = data.rstrip("=")
|
|
69
77
|
# Each 4 base64 chars encode 3 bytes, with the last group potentially having padding
|
|
70
78
|
exact_groups = len(data_without_padding) // 4
|
|
71
79
|
remaining_chars = len(data_without_padding) % 4
|
|
72
|
-
|
|
80
|
+
|
|
73
81
|
decoded_size = exact_groups * 3
|
|
74
82
|
if remaining_chars == 2:
|
|
75
83
|
decoded_size += 1 # 2 chars = 1 byte
|
|
76
84
|
elif remaining_chars == 3:
|
|
77
85
|
decoded_size += 2 # 3 chars = 2 bytes
|
|
78
|
-
|
|
86
|
+
|
|
79
87
|
if decoded_size > MAX_ATTACHMENT_SIZE:
|
|
80
88
|
size_mb = round(decoded_size / 1024 / 1024, 2)
|
|
81
89
|
max_mb = MAX_ATTACHMENT_SIZE // 1024 // 1024
|
|
@@ -88,23 +96,23 @@ def _validate_filename(name: Optional[str]) -> None:
|
|
|
88
96
|
"""Validate filename for security and length constraints."""
|
|
89
97
|
if not name:
|
|
90
98
|
return
|
|
91
|
-
|
|
99
|
+
|
|
92
100
|
if len(name) > MAX_FILENAME_LENGTH:
|
|
93
101
|
raise AttachmentValidationError(
|
|
94
102
|
f"Filename length ({len(name)}) exceeds maximum allowed length ({MAX_FILENAME_LENGTH})"
|
|
95
103
|
)
|
|
96
|
-
|
|
104
|
+
|
|
97
105
|
# Check for dangerous characters and control characters
|
|
98
106
|
dangerous_chars = re.compile(r'[<>:"|?*]')
|
|
99
|
-
control_chars = re.compile(r
|
|
100
|
-
|
|
107
|
+
control_chars = re.compile(r"[\x00-\x1f]")
|
|
108
|
+
|
|
101
109
|
if dangerous_chars.search(name) or control_chars.search(name):
|
|
102
|
-
raise AttachmentValidationError(
|
|
103
|
-
|
|
110
|
+
raise AttachmentValidationError("Filename contains invalid characters")
|
|
111
|
+
|
|
104
112
|
# Check for path traversal attempts
|
|
105
|
-
if
|
|
113
|
+
if ".." in name or "/" in name or "\\" in name:
|
|
106
114
|
raise AttachmentValidationError(
|
|
107
|
-
|
|
115
|
+
"Filename cannot contain path separators or traversal sequences"
|
|
108
116
|
)
|
|
109
117
|
|
|
110
118
|
|
|
@@ -113,10 +121,10 @@ def _validate_mime_type(mime_type: Optional[str], allowed_types: List[str], kind
|
|
|
113
121
|
if mime_type:
|
|
114
122
|
# Normalize the input mime_type
|
|
115
123
|
normalized_mime_type = mime_type.lower().strip()
|
|
116
|
-
|
|
124
|
+
|
|
117
125
|
# Normalize the allowed types list
|
|
118
126
|
normalized_allowed_types = {t.lower().strip() for t in allowed_types}
|
|
119
|
-
|
|
127
|
+
|
|
120
128
|
if normalized_mime_type not in normalized_allowed_types:
|
|
121
129
|
raise AttachmentValidationError(
|
|
122
130
|
f"MIME type '{mime_type}' is not allowed for {kind} attachments. "
|
|
@@ -128,33 +136,33 @@ def _validate_url(url: Optional[str]) -> None:
|
|
|
128
136
|
"""Validate URL format and protocol."""
|
|
129
137
|
if not url:
|
|
130
138
|
return
|
|
131
|
-
|
|
139
|
+
|
|
132
140
|
try:
|
|
133
141
|
parsed = urlparse(url)
|
|
134
|
-
allowed_protocols = [
|
|
135
|
-
|
|
142
|
+
allowed_protocols = ["http", "https", "data"]
|
|
143
|
+
|
|
136
144
|
if parsed.scheme not in allowed_protocols:
|
|
137
145
|
raise AttachmentValidationError(
|
|
138
146
|
f"URL protocol '{parsed.scheme}' is not allowed. "
|
|
139
147
|
f"Allowed protocols: {', '.join(allowed_protocols)}"
|
|
140
148
|
)
|
|
141
|
-
|
|
149
|
+
|
|
142
150
|
# Additional validation for data URLs
|
|
143
|
-
if parsed.scheme ==
|
|
151
|
+
if parsed.scheme == "data":
|
|
144
152
|
# For data URLs, the "path" component in urlparse contains the mediatype and data
|
|
145
153
|
# Proper data URL format: mediatype[;charset][;base64],data
|
|
146
|
-
data_content_pattern = re.compile(r
|
|
154
|
+
data_content_pattern = re.compile(r"^([^;,]+)(;[^;,]+)*(;base64)?,(.+)$")
|
|
147
155
|
data_content = parsed.path
|
|
148
|
-
|
|
156
|
+
|
|
149
157
|
# Some URLs might have query components that are part of the data
|
|
150
158
|
if parsed.query:
|
|
151
159
|
data_content += "?" + parsed.query
|
|
152
|
-
|
|
160
|
+
|
|
153
161
|
if not data_content_pattern.match(data_content):
|
|
154
162
|
raise AttachmentValidationError(
|
|
155
|
-
|
|
163
|
+
"Invalid data URL format: must match mediatype[;charset][;base64],data pattern"
|
|
156
164
|
)
|
|
157
|
-
|
|
165
|
+
|
|
158
166
|
except ValueError as e:
|
|
159
167
|
raise AttachmentValidationError(f"Invalid URL: {e}")
|
|
160
168
|
|
|
@@ -163,16 +171,16 @@ def _process_base64_data(data: Union[bytes, str, None]) -> Optional[str]:
|
|
|
163
171
|
"""Process and validate base64 data."""
|
|
164
172
|
if not data:
|
|
165
173
|
return None
|
|
166
|
-
|
|
174
|
+
|
|
167
175
|
if isinstance(data, bytes):
|
|
168
|
-
base64_str = base64.b64encode(data).decode(
|
|
176
|
+
base64_str = base64.b64encode(data).decode("ascii")
|
|
169
177
|
else:
|
|
170
178
|
base64_str = data
|
|
171
|
-
|
|
179
|
+
|
|
172
180
|
# Validate base64 format if it was provided as string
|
|
173
181
|
if isinstance(data, str) and not _validate_base64(base64_str):
|
|
174
|
-
raise AttachmentValidationError(
|
|
175
|
-
|
|
182
|
+
raise AttachmentValidationError("Invalid base64 data format")
|
|
183
|
+
|
|
176
184
|
return base64_str
|
|
177
185
|
|
|
178
186
|
|
|
@@ -180,46 +188,40 @@ def make_image_attachment(
|
|
|
180
188
|
data: Union[bytes, str, None] = None,
|
|
181
189
|
url: Optional[str] = None,
|
|
182
190
|
mime_type: Optional[str] = None,
|
|
183
|
-
name: Optional[str] = None
|
|
191
|
+
name: Optional[str] = None,
|
|
184
192
|
) -> Attachment:
|
|
185
193
|
"""
|
|
186
194
|
Create a validated image attachment.
|
|
187
|
-
|
|
195
|
+
|
|
188
196
|
Args:
|
|
189
197
|
data: Raw bytes or base64 string
|
|
190
198
|
url: Remote or data URL
|
|
191
199
|
mime_type: MIME type (e.g., 'image/png')
|
|
192
200
|
name: Optional filename
|
|
193
|
-
|
|
201
|
+
|
|
194
202
|
Returns:
|
|
195
203
|
Validated Attachment object
|
|
196
|
-
|
|
204
|
+
|
|
197
205
|
Raises:
|
|
198
206
|
AttachmentValidationError: If validation fails
|
|
199
207
|
"""
|
|
200
208
|
# Validate inputs
|
|
201
209
|
_validate_filename(name)
|
|
202
210
|
_validate_url(url)
|
|
203
|
-
_validate_mime_type(mime_type, ALLOWED_IMAGE_MIME_TYPES,
|
|
204
|
-
|
|
211
|
+
_validate_mime_type(mime_type, ALLOWED_IMAGE_MIME_TYPES, "image")
|
|
212
|
+
|
|
205
213
|
# Process data to base64 first, so we can validate size for both bytes and string inputs
|
|
206
214
|
base64_data = _process_base64_data(data)
|
|
207
|
-
|
|
215
|
+
|
|
208
216
|
# Validate size if we have data
|
|
209
217
|
if base64_data:
|
|
210
218
|
_validate_attachment_size(base64_data)
|
|
211
|
-
|
|
219
|
+
|
|
212
220
|
# Ensure at least one content source
|
|
213
221
|
if not url and not base64_data:
|
|
214
|
-
raise AttachmentValidationError(
|
|
215
|
-
|
|
216
|
-
return Attachment(
|
|
217
|
-
kind='image',
|
|
218
|
-
mime_type=mime_type,
|
|
219
|
-
name=name,
|
|
220
|
-
url=url,
|
|
221
|
-
data=base64_data
|
|
222
|
-
)
|
|
222
|
+
raise AttachmentValidationError("Image attachment must have either url or data")
|
|
223
|
+
|
|
224
|
+
return Attachment(kind="image", mime_type=mime_type, name=name, url=url, data=base64_data)
|
|
223
225
|
|
|
224
226
|
|
|
225
227
|
def make_file_attachment(
|
|
@@ -227,50 +229,45 @@ def make_file_attachment(
|
|
|
227
229
|
url: Optional[str] = None,
|
|
228
230
|
mime_type: Optional[str] = None,
|
|
229
231
|
name: Optional[str] = None,
|
|
230
|
-
format: Optional[str] = None
|
|
232
|
+
format: Optional[str] = None,
|
|
231
233
|
) -> Attachment:
|
|
232
234
|
"""
|
|
233
235
|
Create a validated file attachment.
|
|
234
|
-
|
|
236
|
+
|
|
235
237
|
Args:
|
|
236
238
|
data: Raw bytes or base64 string
|
|
237
239
|
url: Remote or data URL
|
|
238
240
|
mime_type: MIME type
|
|
239
241
|
name: Optional filename
|
|
240
242
|
format: Optional format identifier (e.g., 'pdf', 'txt')
|
|
241
|
-
|
|
243
|
+
|
|
242
244
|
Returns:
|
|
243
245
|
Validated Attachment object
|
|
244
|
-
|
|
246
|
+
|
|
245
247
|
Raises:
|
|
246
248
|
AttachmentValidationError: If validation fails
|
|
247
249
|
"""
|
|
248
250
|
# Validate inputs
|
|
249
251
|
_validate_filename(name)
|
|
250
252
|
_validate_url(url)
|
|
251
|
-
|
|
253
|
+
|
|
252
254
|
# Process data to base64 first, so we can validate size for both bytes and string inputs
|
|
253
255
|
base64_data = _process_base64_data(data)
|
|
254
|
-
|
|
256
|
+
|
|
255
257
|
# Validate size if we have data
|
|
256
258
|
if base64_data:
|
|
257
259
|
_validate_attachment_size(base64_data)
|
|
258
|
-
|
|
260
|
+
|
|
259
261
|
# Ensure at least one content source
|
|
260
262
|
if not url and not base64_data:
|
|
261
|
-
raise AttachmentValidationError(
|
|
262
|
-
|
|
263
|
+
raise AttachmentValidationError("File attachment must have either url or data")
|
|
264
|
+
|
|
263
265
|
# Validate format if provided
|
|
264
266
|
if format and len(format) > MAX_FORMAT_LENGTH:
|
|
265
|
-
raise AttachmentValidationError(
|
|
266
|
-
|
|
267
|
+
raise AttachmentValidationError("File format must be 10 characters or less")
|
|
268
|
+
|
|
267
269
|
return Attachment(
|
|
268
|
-
kind=
|
|
269
|
-
mime_type=mime_type,
|
|
270
|
-
name=name,
|
|
271
|
-
url=url,
|
|
272
|
-
data=base64_data,
|
|
273
|
-
format=format
|
|
270
|
+
kind="file", mime_type=mime_type, name=name, url=url, data=base64_data, format=format
|
|
274
271
|
)
|
|
275
272
|
|
|
276
273
|
|
|
@@ -280,11 +277,11 @@ def make_document_attachment(
|
|
|
280
277
|
mime_type: Optional[str] = None,
|
|
281
278
|
name: Optional[str] = None,
|
|
282
279
|
format: Optional[str] = None,
|
|
283
|
-
use_litellm_format: Optional[bool] = None
|
|
280
|
+
use_litellm_format: Optional[bool] = None,
|
|
284
281
|
) -> Attachment:
|
|
285
282
|
"""
|
|
286
283
|
Create a validated document attachment.
|
|
287
|
-
|
|
284
|
+
|
|
288
285
|
Args:
|
|
289
286
|
data: Raw bytes or base64 string
|
|
290
287
|
url: Remote or data URL
|
|
@@ -292,93 +289,91 @@ def make_document_attachment(
|
|
|
292
289
|
name: Optional filename
|
|
293
290
|
format: Optional format identifier
|
|
294
291
|
use_litellm_format: Whether to use LiteLLM native format
|
|
295
|
-
|
|
292
|
+
|
|
296
293
|
Returns:
|
|
297
294
|
Validated Attachment object
|
|
298
|
-
|
|
295
|
+
|
|
299
296
|
Raises:
|
|
300
297
|
AttachmentValidationError: If validation fails
|
|
301
298
|
"""
|
|
302
299
|
# Additional validation for documents
|
|
303
|
-
_validate_mime_type(mime_type, ALLOWED_DOCUMENT_MIME_TYPES,
|
|
304
|
-
|
|
300
|
+
_validate_mime_type(mime_type, ALLOWED_DOCUMENT_MIME_TYPES, "document")
|
|
301
|
+
|
|
305
302
|
attachment = make_file_attachment(
|
|
306
|
-
data=data,
|
|
307
|
-
url=url,
|
|
308
|
-
mime_type=mime_type,
|
|
309
|
-
name=name,
|
|
310
|
-
format=format
|
|
303
|
+
data=data, url=url, mime_type=mime_type, name=name, format=format
|
|
311
304
|
)
|
|
312
|
-
|
|
305
|
+
|
|
313
306
|
return Attachment(
|
|
314
|
-
kind=
|
|
307
|
+
kind="document",
|
|
315
308
|
mime_type=attachment.mime_type,
|
|
316
309
|
name=attachment.name,
|
|
317
310
|
url=attachment.url,
|
|
318
311
|
data=attachment.data,
|
|
319
312
|
format=attachment.format,
|
|
320
|
-
use_litellm_format=use_litellm_format
|
|
313
|
+
use_litellm_format=use_litellm_format,
|
|
321
314
|
)
|
|
322
315
|
|
|
323
316
|
|
|
324
317
|
def validate_attachment(attachment: Attachment) -> None:
|
|
325
318
|
"""
|
|
326
319
|
Validate an existing attachment object.
|
|
327
|
-
|
|
320
|
+
|
|
328
321
|
Args:
|
|
329
322
|
attachment: Attachment to validate
|
|
330
|
-
|
|
323
|
+
|
|
331
324
|
Raises:
|
|
332
325
|
AttachmentValidationError: If validation fails
|
|
333
326
|
"""
|
|
334
327
|
try:
|
|
335
328
|
if not attachment.url and not attachment.data:
|
|
336
329
|
raise AttachmentValidationError(
|
|
337
|
-
|
|
338
|
-
field='url/data'
|
|
330
|
+
"Attachment must have either url or data", field="url/data"
|
|
339
331
|
)
|
|
340
|
-
|
|
332
|
+
|
|
341
333
|
if attachment.name:
|
|
342
334
|
try:
|
|
343
335
|
_validate_filename(attachment.name)
|
|
344
336
|
except AttachmentValidationError as e:
|
|
345
|
-
raise AttachmentValidationError(f"Invalid filename: {e}", field=
|
|
346
|
-
|
|
337
|
+
raise AttachmentValidationError(f"Invalid filename: {e}", field="name") from e
|
|
338
|
+
|
|
347
339
|
if attachment.url:
|
|
348
340
|
try:
|
|
349
341
|
_validate_url(attachment.url)
|
|
350
342
|
except AttachmentValidationError as e:
|
|
351
|
-
raise AttachmentValidationError(f"Invalid URL: {e}", field=
|
|
352
|
-
|
|
343
|
+
raise AttachmentValidationError(f"Invalid URL: {e}", field="url") from e
|
|
344
|
+
|
|
353
345
|
if attachment.data:
|
|
354
346
|
try:
|
|
355
347
|
_validate_attachment_size(attachment.data)
|
|
356
348
|
except AttachmentValidationError as e:
|
|
357
|
-
raise AttachmentValidationError(f"Size validation failed: {e}", field=
|
|
358
|
-
|
|
349
|
+
raise AttachmentValidationError(f"Size validation failed: {e}", field="data") from e
|
|
350
|
+
|
|
359
351
|
if not _validate_base64(attachment.data):
|
|
360
352
|
raise AttachmentValidationError(
|
|
361
|
-
|
|
362
|
-
field='data'
|
|
353
|
+
"Invalid base64 data format in attachment", field="data"
|
|
363
354
|
)
|
|
364
|
-
|
|
355
|
+
|
|
365
356
|
# Validate MIME type based on attachment kind
|
|
366
|
-
if attachment.kind ==
|
|
357
|
+
if attachment.kind == "image":
|
|
367
358
|
try:
|
|
368
|
-
_validate_mime_type(attachment.mime_type, ALLOWED_IMAGE_MIME_TYPES,
|
|
359
|
+
_validate_mime_type(attachment.mime_type, ALLOWED_IMAGE_MIME_TYPES, "image")
|
|
369
360
|
except AttachmentValidationError as e:
|
|
370
|
-
raise AttachmentValidationError(
|
|
371
|
-
|
|
361
|
+
raise AttachmentValidationError(
|
|
362
|
+
f"Image MIME type validation failed: {e}", field="mime_type"
|
|
363
|
+
) from e
|
|
364
|
+
elif attachment.kind == "document":
|
|
372
365
|
try:
|
|
373
|
-
_validate_mime_type(attachment.mime_type, ALLOWED_DOCUMENT_MIME_TYPES,
|
|
366
|
+
_validate_mime_type(attachment.mime_type, ALLOWED_DOCUMENT_MIME_TYPES, "document")
|
|
374
367
|
except AttachmentValidationError as e:
|
|
375
|
-
raise AttachmentValidationError(
|
|
376
|
-
|
|
368
|
+
raise AttachmentValidationError(
|
|
369
|
+
f"Document MIME type validation failed: {e}", field="mime_type"
|
|
370
|
+
) from e
|
|
371
|
+
elif attachment.kind == "file":
|
|
377
372
|
# Files can have any MIME type, but still validate format
|
|
378
373
|
if attachment.format and len(attachment.format) > MAX_FORMAT_LENGTH:
|
|
379
374
|
raise AttachmentValidationError(
|
|
380
375
|
f'File format "{attachment.format}" exceeds maximum length of {MAX_FORMAT_LENGTH} characters',
|
|
381
|
-
field=
|
|
376
|
+
field="format",
|
|
382
377
|
)
|
|
383
378
|
except AttachmentValidationError:
|
|
384
379
|
raise
|
|
@@ -394,8 +389,8 @@ def assert_non_empty_attachment(attachment: Attachment) -> None:
|
|
|
394
389
|
|
|
395
390
|
# Export validation constants for external use
|
|
396
391
|
ATTACHMENT_LIMITS = {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
}
|
|
392
|
+
"MAX_SIZE": MAX_ATTACHMENT_SIZE,
|
|
393
|
+
"MAX_FILENAME_LENGTH": MAX_FILENAME_LENGTH,
|
|
394
|
+
"ALLOWED_IMAGE_MIME_TYPES": ALLOWED_IMAGE_MIME_TYPES,
|
|
395
|
+
"ALLOWED_DOCUMENT_MIME_TYPES": ALLOWED_DOCUMENT_MIME_TYPES,
|
|
396
|
+
}
|