uploads.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719
  1. # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
  2. from __future__ import annotations
  3. import io
  4. import os
  5. import logging
  6. import builtins
  7. from typing import overload
  8. from pathlib import Path
  9. import anyio
  10. import httpx
  11. from ... import _legacy_response
  12. from .parts import (
  13. Parts,
  14. AsyncParts,
  15. PartsWithRawResponse,
  16. AsyncPartsWithRawResponse,
  17. PartsWithStreamingResponse,
  18. AsyncPartsWithStreamingResponse,
  19. )
  20. from ...types import FilePurpose, upload_create_params, upload_complete_params
  21. from ..._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
  22. from ..._utils import maybe_transform, async_maybe_transform
  23. from ..._compat import cached_property
  24. from ..._resource import SyncAPIResource, AsyncAPIResource
  25. from ..._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper
  26. from ..._base_client import make_request_options
  27. from ...types.upload import Upload
  28. from ...types.file_purpose import FilePurpose
  29. __all__ = ["Uploads", "AsyncUploads"]
  30. # 64MB
  31. DEFAULT_PART_SIZE = 64 * 1024 * 1024
  32. log: logging.Logger = logging.getLogger(__name__)
  33. class Uploads(SyncAPIResource):
  34. @cached_property
  35. def parts(self) -> Parts:
  36. return Parts(self._client)
  37. @cached_property
  38. def with_raw_response(self) -> UploadsWithRawResponse:
  39. """
  40. This property can be used as a prefix for any HTTP method call to return
  41. the raw response object instead of the parsed content.
  42. For more information, see https://www.github.com/openai/openai-python#accessing-raw-response-data-eg-headers
  43. """
  44. return UploadsWithRawResponse(self)
  45. @cached_property
  46. def with_streaming_response(self) -> UploadsWithStreamingResponse:
  47. """
  48. An alternative to `.with_raw_response` that doesn't eagerly read the response body.
  49. For more information, see https://www.github.com/openai/openai-python#with_streaming_response
  50. """
  51. return UploadsWithStreamingResponse(self)
  52. @overload
  53. def upload_file_chunked(
  54. self,
  55. *,
  56. file: os.PathLike[str],
  57. mime_type: str,
  58. purpose: FilePurpose,
  59. bytes: int | None = None,
  60. part_size: int | None = None,
  61. md5: str | Omit = omit,
  62. ) -> Upload:
  63. """Splits a file into multiple 64MB parts and uploads them sequentially."""
  64. @overload
  65. def upload_file_chunked(
  66. self,
  67. *,
  68. file: bytes,
  69. filename: str,
  70. bytes: int,
  71. mime_type: str,
  72. purpose: FilePurpose,
  73. part_size: int | None = None,
  74. md5: str | Omit = omit,
  75. ) -> Upload:
  76. """Splits an in-memory file into multiple 64MB parts and uploads them sequentially."""
  77. def upload_file_chunked(
  78. self,
  79. *,
  80. file: os.PathLike[str] | bytes,
  81. mime_type: str,
  82. purpose: FilePurpose,
  83. filename: str | None = None,
  84. bytes: int | None = None,
  85. part_size: int | None = None,
  86. md5: str | Omit = omit,
  87. ) -> Upload:
  88. """Splits the given file into multiple parts and uploads them sequentially.
  89. ```py
  90. from pathlib import Path
  91. client.uploads.upload_file(
  92. file=Path("my-paper.pdf"),
  93. mime_type="pdf",
  94. purpose="assistants",
  95. )
  96. ```
  97. """
  98. if isinstance(file, builtins.bytes):
  99. if filename is None:
  100. raise TypeError("The `filename` argument must be given for in-memory files")
  101. if bytes is None:
  102. raise TypeError("The `bytes` argument must be given for in-memory files")
  103. else:
  104. if not isinstance(file, Path):
  105. file = Path(file)
  106. if not filename:
  107. filename = file.name
  108. if bytes is None:
  109. bytes = file.stat().st_size
  110. upload = self.create(
  111. bytes=bytes,
  112. filename=filename,
  113. mime_type=mime_type,
  114. purpose=purpose,
  115. )
  116. part_ids: list[str] = []
  117. if part_size is None:
  118. part_size = DEFAULT_PART_SIZE
  119. if isinstance(file, builtins.bytes):
  120. buf: io.FileIO | io.BytesIO = io.BytesIO(file)
  121. else:
  122. buf = io.FileIO(file)
  123. try:
  124. while True:
  125. data = buf.read(part_size)
  126. if not data:
  127. # EOF
  128. break
  129. part = self.parts.create(upload_id=upload.id, data=data)
  130. log.info("Uploaded part %s for upload %s", part.id, upload.id)
  131. part_ids.append(part.id)
  132. finally:
  133. buf.close()
  134. return self.complete(upload_id=upload.id, part_ids=part_ids, md5=md5)
  135. def create(
  136. self,
  137. *,
  138. bytes: int,
  139. filename: str,
  140. mime_type: str,
  141. purpose: FilePurpose,
  142. expires_after: upload_create_params.ExpiresAfter | Omit = omit,
  143. # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
  144. # The extra values given here take precedence over values defined on the client or passed to this method.
  145. extra_headers: Headers | None = None,
  146. extra_query: Query | None = None,
  147. extra_body: Body | None = None,
  148. timeout: float | httpx.Timeout | None | NotGiven = not_given,
  149. ) -> Upload:
  150. """
  151. Creates an intermediate
  152. [Upload](https://platform.openai.com/docs/api-reference/uploads/object) object
  153. that you can add
  154. [Parts](https://platform.openai.com/docs/api-reference/uploads/part-object) to.
  155. Currently, an Upload can accept at most 8 GB in total and expires after an hour
  156. after you create it.
  157. Once you complete the Upload, we will create a
  158. [File](https://platform.openai.com/docs/api-reference/files/object) object that
  159. contains all the parts you uploaded. This File is usable in the rest of our
  160. platform as a regular File object.
  161. For certain `purpose` values, the correct `mime_type` must be specified. Please
  162. refer to documentation for the
  163. [supported MIME types for your use case](https://platform.openai.com/docs/assistants/tools/file-search#supported-files).
  164. For guidance on the proper filename extensions for each purpose, please follow
  165. the documentation on
  166. [creating a File](https://platform.openai.com/docs/api-reference/files/create).
  167. Args:
  168. bytes: The number of bytes in the file you are uploading.
  169. filename: The name of the file to upload.
  170. mime_type: The MIME type of the file.
  171. This must fall within the supported MIME types for your file purpose. See the
  172. supported MIME types for assistants and vision.
  173. purpose: The intended purpose of the uploaded file.
  174. See the
  175. [documentation on File purposes](https://platform.openai.com/docs/api-reference/files/create#files-create-purpose).
  176. expires_after: The expiration policy for a file. By default, files with `purpose=batch` expire
  177. after 30 days and all other files are persisted until they are manually deleted.
  178. extra_headers: Send extra headers
  179. extra_query: Add additional query parameters to the request
  180. extra_body: Add additional JSON properties to the request
  181. timeout: Override the client-level default timeout for this request, in seconds
  182. """
  183. return self._post(
  184. "/uploads",
  185. body=maybe_transform(
  186. {
  187. "bytes": bytes,
  188. "filename": filename,
  189. "mime_type": mime_type,
  190. "purpose": purpose,
  191. "expires_after": expires_after,
  192. },
  193. upload_create_params.UploadCreateParams,
  194. ),
  195. options=make_request_options(
  196. extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
  197. ),
  198. cast_to=Upload,
  199. )
  200. def cancel(
  201. self,
  202. upload_id: str,
  203. *,
  204. # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
  205. # The extra values given here take precedence over values defined on the client or passed to this method.
  206. extra_headers: Headers | None = None,
  207. extra_query: Query | None = None,
  208. extra_body: Body | None = None,
  209. timeout: float | httpx.Timeout | None | NotGiven = not_given,
  210. ) -> Upload:
  211. """Cancels the Upload.
  212. No Parts may be added after an Upload is cancelled.
  213. Args:
  214. extra_headers: Send extra headers
  215. extra_query: Add additional query parameters to the request
  216. extra_body: Add additional JSON properties to the request
  217. timeout: Override the client-level default timeout for this request, in seconds
  218. """
  219. if not upload_id:
  220. raise ValueError(f"Expected a non-empty value for `upload_id` but received {upload_id!r}")
  221. return self._post(
  222. f"/uploads/{upload_id}/cancel",
  223. options=make_request_options(
  224. extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
  225. ),
  226. cast_to=Upload,
  227. )
  228. def complete(
  229. self,
  230. upload_id: str,
  231. *,
  232. part_ids: SequenceNotStr[str],
  233. md5: str | Omit = omit,
  234. # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
  235. # The extra values given here take precedence over values defined on the client or passed to this method.
  236. extra_headers: Headers | None = None,
  237. extra_query: Query | None = None,
  238. extra_body: Body | None = None,
  239. timeout: float | httpx.Timeout | None | NotGiven = not_given,
  240. ) -> Upload:
  241. """
  242. Completes the
  243. [Upload](https://platform.openai.com/docs/api-reference/uploads/object).
  244. Within the returned Upload object, there is a nested
  245. [File](https://platform.openai.com/docs/api-reference/files/object) object that
  246. is ready to use in the rest of the platform.
  247. You can specify the order of the Parts by passing in an ordered list of the Part
  248. IDs.
  249. The number of bytes uploaded upon completion must match the number of bytes
  250. initially specified when creating the Upload object. No Parts may be added after
  251. an Upload is completed.
  252. Args:
  253. part_ids: The ordered list of Part IDs.
  254. md5: The optional md5 checksum for the file contents to verify if the bytes uploaded
  255. matches what you expect.
  256. extra_headers: Send extra headers
  257. extra_query: Add additional query parameters to the request
  258. extra_body: Add additional JSON properties to the request
  259. timeout: Override the client-level default timeout for this request, in seconds
  260. """
  261. if not upload_id:
  262. raise ValueError(f"Expected a non-empty value for `upload_id` but received {upload_id!r}")
  263. return self._post(
  264. f"/uploads/{upload_id}/complete",
  265. body=maybe_transform(
  266. {
  267. "part_ids": part_ids,
  268. "md5": md5,
  269. },
  270. upload_complete_params.UploadCompleteParams,
  271. ),
  272. options=make_request_options(
  273. extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
  274. ),
  275. cast_to=Upload,
  276. )
  277. class AsyncUploads(AsyncAPIResource):
  278. @cached_property
  279. def parts(self) -> AsyncParts:
  280. return AsyncParts(self._client)
  281. @cached_property
  282. def with_raw_response(self) -> AsyncUploadsWithRawResponse:
  283. """
  284. This property can be used as a prefix for any HTTP method call to return
  285. the raw response object instead of the parsed content.
  286. For more information, see https://www.github.com/openai/openai-python#accessing-raw-response-data-eg-headers
  287. """
  288. return AsyncUploadsWithRawResponse(self)
  289. @cached_property
  290. def with_streaming_response(self) -> AsyncUploadsWithStreamingResponse:
  291. """
  292. An alternative to `.with_raw_response` that doesn't eagerly read the response body.
  293. For more information, see https://www.github.com/openai/openai-python#with_streaming_response
  294. """
  295. return AsyncUploadsWithStreamingResponse(self)
  296. @overload
  297. async def upload_file_chunked(
  298. self,
  299. *,
  300. file: os.PathLike[str],
  301. mime_type: str,
  302. purpose: FilePurpose,
  303. bytes: int | None = None,
  304. part_size: int | None = None,
  305. md5: str | Omit = omit,
  306. ) -> Upload:
  307. """Splits a file into multiple 64MB parts and uploads them sequentially."""
  308. @overload
  309. async def upload_file_chunked(
  310. self,
  311. *,
  312. file: bytes,
  313. filename: str,
  314. bytes: int,
  315. mime_type: str,
  316. purpose: FilePurpose,
  317. part_size: int | None = None,
  318. md5: str | Omit = omit,
  319. ) -> Upload:
  320. """Splits an in-memory file into multiple 64MB parts and uploads them sequentially."""
  321. async def upload_file_chunked(
  322. self,
  323. *,
  324. file: os.PathLike[str] | bytes,
  325. mime_type: str,
  326. purpose: FilePurpose,
  327. filename: str | None = None,
  328. bytes: int | None = None,
  329. part_size: int | None = None,
  330. md5: str | Omit = omit,
  331. ) -> Upload:
  332. """Splits the given file into multiple parts and uploads them sequentially.
  333. ```py
  334. from pathlib import Path
  335. client.uploads.upload_file(
  336. file=Path("my-paper.pdf"),
  337. mime_type="pdf",
  338. purpose="assistants",
  339. )
  340. ```
  341. """
  342. if isinstance(file, builtins.bytes):
  343. if filename is None:
  344. raise TypeError("The `filename` argument must be given for in-memory files")
  345. if bytes is None:
  346. raise TypeError("The `bytes` argument must be given for in-memory files")
  347. else:
  348. if not isinstance(file, anyio.Path):
  349. file = anyio.Path(file)
  350. if not filename:
  351. filename = file.name
  352. if bytes is None:
  353. stat = await file.stat()
  354. bytes = stat.st_size
  355. upload = await self.create(
  356. bytes=bytes,
  357. filename=filename,
  358. mime_type=mime_type,
  359. purpose=purpose,
  360. )
  361. part_ids: list[str] = []
  362. if part_size is None:
  363. part_size = DEFAULT_PART_SIZE
  364. if isinstance(file, anyio.Path):
  365. fd = await file.open("rb")
  366. async with fd:
  367. while True:
  368. data = await fd.read(part_size)
  369. if not data:
  370. # EOF
  371. break
  372. part = await self.parts.create(upload_id=upload.id, data=data)
  373. log.info("Uploaded part %s for upload %s", part.id, upload.id)
  374. part_ids.append(part.id)
  375. else:
  376. buf = io.BytesIO(file)
  377. try:
  378. while True:
  379. data = buf.read(part_size)
  380. if not data:
  381. # EOF
  382. break
  383. part = await self.parts.create(upload_id=upload.id, data=data)
  384. log.info("Uploaded part %s for upload %s", part.id, upload.id)
  385. part_ids.append(part.id)
  386. finally:
  387. buf.close()
  388. return await self.complete(upload_id=upload.id, part_ids=part_ids, md5=md5)
  389. async def create(
  390. self,
  391. *,
  392. bytes: int,
  393. filename: str,
  394. mime_type: str,
  395. purpose: FilePurpose,
  396. expires_after: upload_create_params.ExpiresAfter | Omit = omit,
  397. # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
  398. # The extra values given here take precedence over values defined on the client or passed to this method.
  399. extra_headers: Headers | None = None,
  400. extra_query: Query | None = None,
  401. extra_body: Body | None = None,
  402. timeout: float | httpx.Timeout | None | NotGiven = not_given,
  403. ) -> Upload:
  404. """
  405. Creates an intermediate
  406. [Upload](https://platform.openai.com/docs/api-reference/uploads/object) object
  407. that you can add
  408. [Parts](https://platform.openai.com/docs/api-reference/uploads/part-object) to.
  409. Currently, an Upload can accept at most 8 GB in total and expires after an hour
  410. after you create it.
  411. Once you complete the Upload, we will create a
  412. [File](https://platform.openai.com/docs/api-reference/files/object) object that
  413. contains all the parts you uploaded. This File is usable in the rest of our
  414. platform as a regular File object.
  415. For certain `purpose` values, the correct `mime_type` must be specified. Please
  416. refer to documentation for the
  417. [supported MIME types for your use case](https://platform.openai.com/docs/assistants/tools/file-search#supported-files).
  418. For guidance on the proper filename extensions for each purpose, please follow
  419. the documentation on
  420. [creating a File](https://platform.openai.com/docs/api-reference/files/create).
  421. Args:
  422. bytes: The number of bytes in the file you are uploading.
  423. filename: The name of the file to upload.
  424. mime_type: The MIME type of the file.
  425. This must fall within the supported MIME types for your file purpose. See the
  426. supported MIME types for assistants and vision.
  427. purpose: The intended purpose of the uploaded file.
  428. See the
  429. [documentation on File purposes](https://platform.openai.com/docs/api-reference/files/create#files-create-purpose).
  430. expires_after: The expiration policy for a file. By default, files with `purpose=batch` expire
  431. after 30 days and all other files are persisted until they are manually deleted.
  432. extra_headers: Send extra headers
  433. extra_query: Add additional query parameters to the request
  434. extra_body: Add additional JSON properties to the request
  435. timeout: Override the client-level default timeout for this request, in seconds
  436. """
  437. return await self._post(
  438. "/uploads",
  439. body=await async_maybe_transform(
  440. {
  441. "bytes": bytes,
  442. "filename": filename,
  443. "mime_type": mime_type,
  444. "purpose": purpose,
  445. "expires_after": expires_after,
  446. },
  447. upload_create_params.UploadCreateParams,
  448. ),
  449. options=make_request_options(
  450. extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
  451. ),
  452. cast_to=Upload,
  453. )
  454. async def cancel(
  455. self,
  456. upload_id: str,
  457. *,
  458. # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
  459. # The extra values given here take precedence over values defined on the client or passed to this method.
  460. extra_headers: Headers | None = None,
  461. extra_query: Query | None = None,
  462. extra_body: Body | None = None,
  463. timeout: float | httpx.Timeout | None | NotGiven = not_given,
  464. ) -> Upload:
  465. """Cancels the Upload.
  466. No Parts may be added after an Upload is cancelled.
  467. Args:
  468. extra_headers: Send extra headers
  469. extra_query: Add additional query parameters to the request
  470. extra_body: Add additional JSON properties to the request
  471. timeout: Override the client-level default timeout for this request, in seconds
  472. """
  473. if not upload_id:
  474. raise ValueError(f"Expected a non-empty value for `upload_id` but received {upload_id!r}")
  475. return await self._post(
  476. f"/uploads/{upload_id}/cancel",
  477. options=make_request_options(
  478. extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
  479. ),
  480. cast_to=Upload,
  481. )
  482. async def complete(
  483. self,
  484. upload_id: str,
  485. *,
  486. part_ids: SequenceNotStr[str],
  487. md5: str | Omit = omit,
  488. # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
  489. # The extra values given here take precedence over values defined on the client or passed to this method.
  490. extra_headers: Headers | None = None,
  491. extra_query: Query | None = None,
  492. extra_body: Body | None = None,
  493. timeout: float | httpx.Timeout | None | NotGiven = not_given,
  494. ) -> Upload:
  495. """
  496. Completes the
  497. [Upload](https://platform.openai.com/docs/api-reference/uploads/object).
  498. Within the returned Upload object, there is a nested
  499. [File](https://platform.openai.com/docs/api-reference/files/object) object that
  500. is ready to use in the rest of the platform.
  501. You can specify the order of the Parts by passing in an ordered list of the Part
  502. IDs.
  503. The number of bytes uploaded upon completion must match the number of bytes
  504. initially specified when creating the Upload object. No Parts may be added after
  505. an Upload is completed.
  506. Args:
  507. part_ids: The ordered list of Part IDs.
  508. md5: The optional md5 checksum for the file contents to verify if the bytes uploaded
  509. matches what you expect.
  510. extra_headers: Send extra headers
  511. extra_query: Add additional query parameters to the request
  512. extra_body: Add additional JSON properties to the request
  513. timeout: Override the client-level default timeout for this request, in seconds
  514. """
  515. if not upload_id:
  516. raise ValueError(f"Expected a non-empty value for `upload_id` but received {upload_id!r}")
  517. return await self._post(
  518. f"/uploads/{upload_id}/complete",
  519. body=await async_maybe_transform(
  520. {
  521. "part_ids": part_ids,
  522. "md5": md5,
  523. },
  524. upload_complete_params.UploadCompleteParams,
  525. ),
  526. options=make_request_options(
  527. extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
  528. ),
  529. cast_to=Upload,
  530. )
  531. class UploadsWithRawResponse:
  532. def __init__(self, uploads: Uploads) -> None:
  533. self._uploads = uploads
  534. self.create = _legacy_response.to_raw_response_wrapper(
  535. uploads.create,
  536. )
  537. self.cancel = _legacy_response.to_raw_response_wrapper(
  538. uploads.cancel,
  539. )
  540. self.complete = _legacy_response.to_raw_response_wrapper(
  541. uploads.complete,
  542. )
  543. @cached_property
  544. def parts(self) -> PartsWithRawResponse:
  545. return PartsWithRawResponse(self._uploads.parts)
  546. class AsyncUploadsWithRawResponse:
  547. def __init__(self, uploads: AsyncUploads) -> None:
  548. self._uploads = uploads
  549. self.create = _legacy_response.async_to_raw_response_wrapper(
  550. uploads.create,
  551. )
  552. self.cancel = _legacy_response.async_to_raw_response_wrapper(
  553. uploads.cancel,
  554. )
  555. self.complete = _legacy_response.async_to_raw_response_wrapper(
  556. uploads.complete,
  557. )
  558. @cached_property
  559. def parts(self) -> AsyncPartsWithRawResponse:
  560. return AsyncPartsWithRawResponse(self._uploads.parts)
  561. class UploadsWithStreamingResponse:
  562. def __init__(self, uploads: Uploads) -> None:
  563. self._uploads = uploads
  564. self.create = to_streamed_response_wrapper(
  565. uploads.create,
  566. )
  567. self.cancel = to_streamed_response_wrapper(
  568. uploads.cancel,
  569. )
  570. self.complete = to_streamed_response_wrapper(
  571. uploads.complete,
  572. )
  573. @cached_property
  574. def parts(self) -> PartsWithStreamingResponse:
  575. return PartsWithStreamingResponse(self._uploads.parts)
  576. class AsyncUploadsWithStreamingResponse:
  577. def __init__(self, uploads: AsyncUploads) -> None:
  578. self._uploads = uploads
  579. self.create = async_to_streamed_response_wrapper(
  580. uploads.create,
  581. )
  582. self.cancel = async_to_streamed_response_wrapper(
  583. uploads.cancel,
  584. )
  585. self.complete = async_to_streamed_response_wrapper(
  586. uploads.complete,
  587. )
  588. @cached_property
  589. def parts(self) -> AsyncPartsWithStreamingResponse:
  590. return AsyncPartsWithStreamingResponse(self._uploads.parts)