|
1 | 1 | import json |
| 2 | +import mimetypes |
| 3 | +import os |
2 | 4 | import time |
3 | 5 | import uuid |
4 | 6 | import asyncio |
5 | | -from typing import Any, Dict, Optional, Type, cast, get_origin |
| 7 | +from typing import Any, Dict, List, Optional, Tuple, Type, cast, get_origin |
6 | 8 |
|
7 | 9 | from getstream.models import APIError |
8 | 10 | from getstream.rate_limit import extract_rate_limit |
|
25 | 27 | import ijson |
26 | 28 |
|
27 | 29 |
|
| 30 | +def _read_file_bytes(file_path: str) -> bytes: |
| 31 | + with open(file_path, "rb") as f: |
| 32 | + return f.read() |
| 33 | + |
| 34 | + |
28 | 35 | def _strip_none(obj): |
29 | 36 | """Recursively remove None values from dicts so unset optional fields |
30 | 37 | are omitted from the JSON body instead of being sent as null.""" |
@@ -305,6 +312,39 @@ def delete( |
305 | 312 | data_type=data_type, |
306 | 313 | ) |
307 | 314 |
|
| 315 | + def _upload_multipart( |
| 316 | + self, |
| 317 | + path: str, |
| 318 | + data_type: Type[T], |
| 319 | + file_path: str, |
| 320 | + *, |
| 321 | + path_params: Optional[Dict[str, str]] = None, |
| 322 | + query_params: Optional[Dict[str, str]] = None, |
| 323 | + form_fields: Optional[List[Tuple[str, str]]] = None, |
| 324 | + ) -> StreamResponse[T]: |
| 325 | + """Send a multipart/form-data upload request, matching Go/PHP SDK behavior.""" |
| 326 | + file_name = os.path.basename(file_path) |
| 327 | + content_type = mimetypes.guess_type(file_path)[0] or "application/octet-stream" |
| 328 | + with open(file_path, "rb") as f: |
| 329 | + file_content = f.read() |
| 330 | + |
| 331 | + files = {"file": (file_name, file_content, content_type)} |
| 332 | + data: Dict[str, str] = {} |
| 333 | + for field_name, field_value in form_fields or []: |
| 334 | + data[field_name] = field_value |
| 335 | + |
| 336 | + kwargs: Dict[str, Any] = {"files": files} |
| 337 | + if data: |
| 338 | + kwargs["data"] = data |
| 339 | + |
| 340 | + return self._request_sync( |
| 341 | + "POST", |
| 342 | + path, |
| 343 | + query_params=query_params, |
| 344 | + kwargs=kwargs | {"path_params": path_params}, |
| 345 | + data_type=data_type, |
| 346 | + ) |
| 347 | + |
308 | 348 | def close(self): |
309 | 349 | """ |
310 | 350 | Close HTTPX client. |
@@ -345,6 +385,39 @@ async def aclose(self): |
345 | 385 | """Close HTTPX async client (closes pools/keep-alives).""" |
346 | 386 | await self.client.aclose() |
347 | 387 |
|
| 388 | + async def _upload_multipart( |
| 389 | + self, |
| 390 | + path: str, |
| 391 | + data_type: Type[T], |
| 392 | + file_path: str, |
| 393 | + *, |
| 394 | + path_params: Optional[Dict[str, str]] = None, |
| 395 | + query_params: Optional[Dict[str, str]] = None, |
| 396 | + form_fields: Optional[List[Tuple[str, str]]] = None, |
| 397 | + ) -> StreamResponse[T]: |
| 398 | + """Send a multipart/form-data upload request, matching Go/PHP SDK behavior.""" |
| 399 | + file_name = os.path.basename(file_path) |
| 400 | + content_type = mimetypes.guess_type(file_path)[0] or "application/octet-stream" |
| 401 | + |
| 402 | + file_content = await asyncio.to_thread(_read_file_bytes, file_path) |
| 403 | + |
| 404 | + files = {"file": (file_name, file_content, content_type)} |
| 405 | + data: Dict[str, str] = {} |
| 406 | + for field_name, field_value in form_fields or []: |
| 407 | + data[field_name] = field_value |
| 408 | + |
| 409 | + kwargs: Dict[str, Any] = {"files": files} |
| 410 | + if data: |
| 411 | + kwargs["data"] = data |
| 412 | + |
| 413 | + return await self._request_async( |
| 414 | + "POST", |
| 415 | + path, |
| 416 | + query_params=query_params, |
| 417 | + kwargs=kwargs | {"path_params": path_params}, |
| 418 | + data_type=data_type, |
| 419 | + ) |
| 420 | + |
348 | 421 | def _endpoint_name(self, path: str) -> str: |
349 | 422 | op = getattr(self, "_operation_name", None) |
350 | 423 | return op or current_operation(self._normalize_endpoint_from_path(path)) or "" |
|
0 commit comments