blob.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. """Schema for Blobs and Blob Loaders.
  2. The goal is to facilitate decoupling of content loading from content parsing code.
  3. In addition, content loading code should provide a lazy loading interface by default.
  4. """
  5. from __future__ import annotations
  6. import contextlib
  7. import mimetypes
  8. from collections.abc import Generator, Mapping
  9. from io import BufferedReader, BytesIO
  10. from pathlib import Path, PurePath
  11. from typing import Any, Optional, Union
  12. from pydantic import BaseModel, ConfigDict, model_validator
  13. PathLike = Union[str, PurePath]
  14. class Blob(BaseModel):
  15. """A blob is used to represent raw data by either reference or value.
  16. Provides an interface to materialize the blob in different representations, and
  17. help to decouple the development of data loaders from the downstream parsing of
  18. the raw data.
  19. Inspired by: https://developer.mozilla.org/en-US/docs/Web/API/Blob
  20. """
  21. data: Union[bytes, str, None] = None # Raw data
  22. mimetype: Optional[str] = None # Not to be confused with a file extension
  23. encoding: str = "utf-8" # Use utf-8 as default encoding, if decoding to string
  24. # Location where the original content was found
  25. # Represent location on the local file system
  26. # Useful for situations where downstream code assumes it must work with file paths
  27. # rather than in-memory content.
  28. path: Optional[PathLike] = None
  29. model_config = ConfigDict(arbitrary_types_allowed=True, frozen=True)
  30. @property
  31. def source(self) -> Optional[str]:
  32. """The source location of the blob as string if known otherwise none."""
  33. return str(self.path) if self.path else None
  34. @model_validator(mode="before")
  35. @classmethod
  36. def check_blob_is_valid(cls, values: Mapping[str, Any]) -> Mapping[str, Any]:
  37. """Verify that either data or path is provided."""
  38. if "data" not in values and "path" not in values:
  39. raise ValueError("Either data or path must be provided")
  40. return values
  41. def as_string(self) -> str:
  42. """Read data as a string."""
  43. if self.data is None and self.path:
  44. return Path(str(self.path)).read_text(encoding=self.encoding)
  45. elif isinstance(self.data, bytes):
  46. return self.data.decode(self.encoding)
  47. elif isinstance(self.data, str):
  48. return self.data
  49. else:
  50. raise ValueError(f"Unable to get string for blob {self}")
  51. def as_bytes(self) -> bytes:
  52. """Read data as bytes."""
  53. if isinstance(self.data, bytes):
  54. return self.data
  55. elif isinstance(self.data, str):
  56. return self.data.encode(self.encoding)
  57. elif self.data is None and self.path:
  58. return Path(str(self.path)).read_bytes()
  59. else:
  60. raise ValueError(f"Unable to get bytes for blob {self}")
  61. @contextlib.contextmanager
  62. def as_bytes_io(self) -> Generator[Union[BytesIO, BufferedReader], None, None]:
  63. """Read data as a byte stream."""
  64. if isinstance(self.data, bytes):
  65. yield BytesIO(self.data)
  66. elif self.data is None and self.path:
  67. with open(str(self.path), "rb") as f:
  68. yield f
  69. else:
  70. raise NotImplementedError(f"Unable to convert blob {self}")
  71. @classmethod
  72. def from_path(
  73. cls,
  74. path: PathLike,
  75. *,
  76. encoding: str = "utf-8",
  77. mime_type: Optional[str] = None,
  78. guess_type: bool = True,
  79. ) -> Blob:
  80. """Load the blob from a path like object.
  81. Args:
  82. path: path like object to file to be read
  83. encoding: Encoding to use if decoding the bytes into a string
  84. mime_type: if provided, will be set as the mime-type of the data
  85. guess_type: If True, the mimetype will be guessed from the file extension,
  86. if a mime-type was not provided
  87. Returns:
  88. Blob instance
  89. """
  90. if mime_type is None and guess_type:
  91. _mimetype = mimetypes.guess_type(path)[0] if guess_type else None
  92. else:
  93. _mimetype = mime_type
  94. # We do not load the data immediately, instead we treat the blob as a
  95. # reference to the underlying data.
  96. return cls(data=None, mimetype=_mimetype, encoding=encoding, path=path)
  97. @classmethod
  98. def from_data(
  99. cls,
  100. data: Union[str, bytes],
  101. *,
  102. encoding: str = "utf-8",
  103. mime_type: Optional[str] = None,
  104. path: Optional[str] = None,
  105. ) -> Blob:
  106. """Initialize the blob from in-memory data.
  107. Args:
  108. data: the in-memory data associated with the blob
  109. encoding: Encoding to use if decoding the bytes into a string
  110. mime_type: if provided, will be set as the mime-type of the data
  111. path: if provided, will be set as the source from which the data came
  112. Returns:
  113. Blob instance
  114. """
  115. return cls(data=data, mimetype=mime_type, encoding=encoding, path=path)
  116. def __repr__(self) -> str:
  117. """Define the blob representation."""
  118. str_repr = f"Blob {id(self)}"
  119. if self.source:
  120. str_repr += f" {self.source}"
  121. return str_repr