_files.py 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. from __future__ import annotations
  2. import io
  3. import os
  4. import pathlib
  5. from typing import overload
  6. from typing_extensions import TypeGuard
  7. import anyio
  8. from ._types import (
  9. FileTypes,
  10. FileContent,
  11. RequestFiles,
  12. HttpxFileTypes,
  13. Base64FileInput,
  14. HttpxFileContent,
  15. HttpxRequestFiles,
  16. )
  17. from ._utils import is_tuple_t, is_mapping_t, is_sequence_t
  18. def is_base64_file_input(obj: object) -> TypeGuard[Base64FileInput]:
  19. return isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike)
  20. def is_file_content(obj: object) -> TypeGuard[FileContent]:
  21. return (
  22. isinstance(obj, bytes) or isinstance(obj, tuple) or isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike)
  23. )
  24. def assert_is_file_content(obj: object, *, key: str | None = None) -> None:
  25. if not is_file_content(obj):
  26. prefix = f"Expected entry at `{key}`" if key is not None else f"Expected file input `{obj!r}`"
  27. raise RuntimeError(
  28. f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead. See https://github.com/openai/openai-python/tree/main#file-uploads"
  29. ) from None
  30. @overload
  31. def to_httpx_files(files: None) -> None: ...
  32. @overload
  33. def to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: ...
  34. def to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None:
  35. if files is None:
  36. return None
  37. if is_mapping_t(files):
  38. files = {key: _transform_file(file) for key, file in files.items()}
  39. elif is_sequence_t(files):
  40. files = [(key, _transform_file(file)) for key, file in files]
  41. else:
  42. raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence")
  43. return files
  44. def _transform_file(file: FileTypes) -> HttpxFileTypes:
  45. if is_file_content(file):
  46. if isinstance(file, os.PathLike):
  47. path = pathlib.Path(file)
  48. return (path.name, path.read_bytes())
  49. return file
  50. if is_tuple_t(file):
  51. return (file[0], read_file_content(file[1]), *file[2:])
  52. raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple")
  53. def read_file_content(file: FileContent) -> HttpxFileContent:
  54. if isinstance(file, os.PathLike):
  55. return pathlib.Path(file).read_bytes()
  56. return file
  57. @overload
  58. async def async_to_httpx_files(files: None) -> None: ...
  59. @overload
  60. async def async_to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: ...
  61. async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None:
  62. if files is None:
  63. return None
  64. if is_mapping_t(files):
  65. files = {key: await _async_transform_file(file) for key, file in files.items()}
  66. elif is_sequence_t(files):
  67. files = [(key, await _async_transform_file(file)) for key, file in files]
  68. else:
  69. raise TypeError("Unexpected file type input {type(files)}, expected mapping or sequence")
  70. return files
  71. async def _async_transform_file(file: FileTypes) -> HttpxFileTypes:
  72. if is_file_content(file):
  73. if isinstance(file, os.PathLike):
  74. path = anyio.Path(file)
  75. return (path.name, await path.read_bytes())
  76. return file
  77. if is_tuple_t(file):
  78. return (file[0], await async_read_file_content(file[1]), *file[2:])
  79. raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple")
  80. async def async_read_file_content(file: FileContent) -> HttpxFileContent:
  81. if isinstance(file, os.PathLike):
  82. return await anyio.Path(file).read_bytes()
  83. return file