Skip to content

Commit d578b42

Browse files
dsfacciniDouweM
andauthored
Add BinaryContent.from_path convenience method (#3482)
Co-authored-by: Douwe Maan <[email protected]>
1 parent 9c70fb5 commit d578b42

File tree

2 files changed

+56
-0
lines changed

2 files changed

+56
-0
lines changed

pydantic_ai_slim/pydantic_ai/messages.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from dataclasses import KW_ONLY, dataclass, field, replace
88
from datetime import datetime
99
from mimetypes import guess_type
10+
from os import PathLike
11+
from pathlib import Path
1012
from typing import TYPE_CHECKING, Annotated, Any, Literal, TypeAlias, cast, overload
1113

1214
import pydantic
@@ -530,6 +532,25 @@ def from_data_uri(cls, data_uri: str) -> BinaryContent:
530532
media_type, data = data_uri[len(prefix) :].split(';base64,', 1)
531533
return cls.narrow_type(cls(data=base64.b64decode(data), media_type=media_type))
532534

535+
@classmethod
536+
def from_path(cls, path: PathLike[str]) -> BinaryContent:
537+
"""Create a `BinaryContent` from a path.
538+
539+
Defaults to 'application/octet-stream' if the media type cannot be inferred.
540+
541+
Raises:
542+
FileNotFoundError: if the file does not exist.
543+
PermissionError: if the file cannot be read.
544+
"""
545+
path = Path(path)
546+
if not path.exists():
547+
raise FileNotFoundError(f'File not found: {path}')
548+
media_type, _ = guess_type(path)
549+
if media_type is None:
550+
media_type = 'application/octet-stream'
551+
552+
return cls.narrow_type(cls(data=path.read_bytes(), media_type=media_type))
553+
533554
@pydantic.computed_field
534555
@property
535556
def identifier(self) -> str:

tests/test_messages.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import sys
22
from datetime import datetime, timezone
3+
from pathlib import Path
34

45
import pytest
56
from inline_snapshot import snapshot
@@ -612,3 +613,37 @@ def test_binary_content_validation_with_optional_identifier():
612613
'identifier': 'foo',
613614
}
614615
)
616+
617+
618+
def test_binary_content_from_path(tmp_path: Path):
619+
# test normal file
620+
test_xml_file = tmp_path / 'test.xml'
621+
test_xml_file.write_text('<think>about trains</think>', encoding='utf-8')
622+
binary_content = BinaryContent.from_path(test_xml_file)
623+
assert binary_content == snapshot(BinaryContent(data=b'<think>about trains</think>', media_type='application/xml'))
624+
625+
# test non-existent file
626+
non_existent_file = tmp_path / 'non-existent.txt'
627+
with pytest.raises(FileNotFoundError, match='File not found:'):
628+
BinaryContent.from_path(non_existent_file)
629+
630+
# test file with unknown media type
631+
test_unknown_file = tmp_path / 'test.unknownext'
632+
test_unknown_file.write_text('some content', encoding='utf-8')
633+
binary_content = BinaryContent.from_path(test_unknown_file)
634+
assert binary_content == snapshot(BinaryContent(data=b'some content', media_type='application/octet-stream'))
635+
636+
# test string path
637+
test_txt_file = tmp_path / 'test.txt'
638+
test_txt_file.write_text('just some text', encoding='utf-8')
639+
string_path = test_txt_file.as_posix()
640+
binary_content = BinaryContent.from_path(string_path) # pyright: ignore[reportArgumentType]
641+
assert binary_content == snapshot(BinaryContent(data=b'just some text', media_type='text/plain'))
642+
643+
# test image file
644+
test_jpg_file = tmp_path / 'test.jpg'
645+
test_jpg_file.write_bytes(b'\xff\xd8\xff\xe0' + b'0' * 100) # minimal JPEG header + padding
646+
binary_content = BinaryContent.from_path(test_jpg_file)
647+
assert binary_content == snapshot(
648+
BinaryImage(data=b'\xff\xd8\xff\xe0' + b'0' * 100, media_type='image/jpeg', _identifier='bc8d49')
649+
)

0 commit comments

Comments
 (0)