|
10 | 10 | import requests
|
11 | 11 | from dataclasses_json import dataclass_json
|
12 | 12 | from requests.exceptions import HTTPError
|
13 |
| -from requests.models import Response |
| 13 | +from requests.models import Response,RequestField, encode_multipart_formdata |
| 14 | + |
| 15 | +from dataclasses import dataclass |
| 16 | +from typing import BinaryIO, List, Optional, TypeVar, Union |
| 17 | +import mimetypes |
14 | 18 |
|
15 | 19 | from servicestack.dtos import IReturn, IReturnVoid, IGet, IPost, IPut, IPatch, \
|
16 | 20 | IDelete, ResponseStatus, EmptyResponse, GetAccessToken, GetAccessTokenResponse
|
@@ -195,6 +199,17 @@ class WebServiceException(Exception):
|
195 | 199 |
|
196 | 200 | T = TypeVar('T')
|
197 | 201 |
|
| 202 | +@dataclass |
| 203 | +class UploadFile: |
| 204 | + field_name: Optional[str] |
| 205 | + file_name: Optional[str] |
| 206 | + content_type: Optional[str] |
| 207 | + stream: BinaryIO |
| 208 | + |
| 209 | + def __post_init__(self): |
| 210 | + if not self.content_type and self.file_name: |
| 211 | + guessed_type = mimetypes.guess_type(self.file_name)[0] |
| 212 | + self.content_type = guessed_type or 'application/octet-stream' |
198 | 213 |
|
199 | 214 | class JsonServiceClient:
|
200 | 215 | base_url: str = None
|
@@ -351,6 +366,71 @@ def send(self, request, method: Any = None, body: Any = None, args: Dict[str, An
|
351 | 366 | args=args,
|
352 | 367 | response_as=response_as))
|
353 | 368 |
|
| 369 | + |
| 370 | + def post_files_with_request(self, request_uri: str, request: Any, |
| 371 | + files: Union[UploadFile, List[UploadFile]]) -> T: |
| 372 | + """ |
| 373 | + Post files with a request DTO using multipart/form-data |
| 374 | +
|
| 375 | + :param request_uri: The request URI |
| 376 | + :param request: The request DTO |
| 377 | + :param files: Single UploadFile or List of UploadFile objects |
| 378 | + :return: Response DTO |
| 379 | + """ |
| 380 | + if isinstance(files, UploadFile): |
| 381 | + files = [files] |
| 382 | + |
| 383 | + # Convert request DTO to dict |
| 384 | + request_dict = to_dict(request, key_case=clean_camelcase) |
| 385 | + |
| 386 | + # Prepare the files dictionary for requests |
| 387 | + files_dict = {} |
| 388 | + for file in files: |
| 389 | + files_dict[file.field_name or 'upload'] = ( |
| 390 | + file.file_name, |
| 391 | + file.stream, |
| 392 | + file.content_type or 'application/octet-stream' |
| 393 | + ) |
| 394 | + |
| 395 | + # Prepare headers |
| 396 | + headers = self.headers.copy() |
| 397 | + if self.bearer_token: |
| 398 | + headers['Authorization'] = f'Bearer {self.bearer_token}' |
| 399 | + |
| 400 | + url = self.to_absolute_url(request_uri) |
| 401 | + |
| 402 | + try: |
| 403 | + # Send the multipart request |
| 404 | + response = self._session.post( |
| 405 | + url, |
| 406 | + data=request_dict, |
| 407 | + files=files_dict, |
| 408 | + headers=headers, |
| 409 | + verify=self._session.verify |
| 410 | + ) |
| 411 | + |
| 412 | + # Handle errors |
| 413 | + response.raise_for_status() |
| 414 | + |
| 415 | + # Parse response |
| 416 | + response_type = _resolve_response_type(request) |
| 417 | + if response_type is None: |
| 418 | + return response.json() |
| 419 | + |
| 420 | + if response_type is str: |
| 421 | + return response.text |
| 422 | + |
| 423 | + if response_type is bytes: |
| 424 | + return response.content |
| 425 | + |
| 426 | + return from_json(response_type, response.text) |
| 427 | + |
| 428 | + finally: |
| 429 | + # Close all file streams |
| 430 | + for file in files: |
| 431 | + file.stream.close() |
| 432 | + |
| 433 | + |
354 | 434 | @staticmethod
|
355 | 435 | def assert_valid_batch_request(request_dtos: list):
|
356 | 436 | if not isinstance(request_dtos, list):
|
|
0 commit comments