Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/openai/_utils/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def _extract_items(
try:
# Remove the field if there are no more dict keys in the path,
# only "<array>" traversal markers or end.
if all(p == "<array>" for p in path[index:]):
if all(p == "<array>" for p in path[index:]) and (index == len(path) or is_list(obj[key])):
item = obj.pop(key)
else:
item = obj[key]
Expand Down
10 changes: 10 additions & 0 deletions tests/test_extract_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ def test_top_level_file_array() -> None:
assert query == {"title": "hello"}


@pytest.mark.parametrize("file", [b"zip contents", ("skill.zip", b"zip contents")])
def test_single_file_fallback_after_array_path(file: FileTypes) -> None:
query = {"files": file, "title": "hello"}
assert extract_files(query, paths=[["files", "<array>"]]) == []
assert query == {"files": file, "title": "hello"}

assert extract_files(query, paths=[["files", "<array>"], ["files"]]) == [("files", file)]
assert query == {"title": "hello"}


@pytest.mark.parametrize(
"query,paths,expected",
[
Expand Down
42 changes: 42 additions & 0 deletions tests/test_files.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,56 @@
import io
import zipfile
from pathlib import Path

import anyio
import httpx2
import pytest

from openai import OpenAI, AsyncOpenAI
from openai._files import to_httpx_files, deepcopy_with_paths, async_to_httpx_files
from openai._utils import extract_files

readme_path = Path(__file__).parent.parent.joinpath("README.md")


@pytest.mark.asyncio
@pytest.mark.parametrize("is_async", [False, True])
@pytest.mark.parametrize("as_list", [False, True])
async def test_skills_upload_zip(is_async: bool, as_list: bool) -> None:
buffer = io.BytesIO()
with zipfile.ZipFile(buffer, "w") as archive:
archive.writestr("SKILL.md", "---\nname: example\ndescription: Test skill\n---\n# Example\n")
content = buffer.getvalue()
file = ("skill.zip", content)
files = [file] if as_list else file
transport = httpx2.MockTransport(lambda _request: httpx2.Response(200, json={}))
client = (
AsyncOpenAI(
api_key="fake-key", base_url="https://example.test", http_client=httpx2.AsyncClient(transport=transport)
)
if is_async
else OpenAI(api_key="fake-key", base_url="https://example.test", http_client=httpx2.Client(transport=transport))
)
try:
response = (
await client.skills.with_raw_response.create(files=files)
if isinstance(client, AsyncOpenAI)
else client.skills.with_raw_response.create(files=files)
)
request = response.http_request
assert request.url.path == "/skills"
assert request.headers["content-type"].startswith("multipart/form-data;")
expected_field = b'name="files[]"' if as_list else b'name="files"'
assert expected_field in request.content
assert b'filename="skill.zip"' in request.content
assert request.content.count(content) == 1
finally:
if isinstance(client, AsyncOpenAI):
await client.close()
else:
client.close()


def test_pathlib_includes_file_name() -> None:
result = to_httpx_files({"file": readme_path})
assert result == {"file": ("README.md", readme_path.read_bytes())}
Expand Down