Skip to content
Merged
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
189 changes: 189 additions & 0 deletions test/api/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,192 @@ def test_improper_function():

with pytest.raises(EtlApiException):
TestClient(wrap_in_fastapi(func=test_fn, plugin_id="mock_plugin"))


@pytest.mark.parametrize(
"file_data", mock_file_data, ids=[type(fd).__name__ for fd in mock_file_data]
)
def test_exception_with_none_status_code(file_data):
"""Test that exceptions with status_code=None are handled correctly."""
from test.assets.exception_status_code import (
function_raises_exception_with_none_status_code as test_fn,
)

client = TestClient(wrap_in_fastapi(func=test_fn, plugin_id="mock_plugin"))

post_body = {"file_data": file_data.model_dump()}
resp = client.post("/invoke", json=post_body)
resp_content = resp.json()
invoke_response = InvokeResponse.model_validate(resp_content)

# Should default to 500 when status_code is None
assert invoke_response.status_code == 500
assert "ExceptionWithNoneStatusCode" in invoke_response.status_code_text
assert "Test exception with None status_code" in invoke_response.status_code_text


@pytest.mark.parametrize(
"file_data", mock_file_data, ids=[type(fd).__name__ for fd in mock_file_data]
)
def test_exception_with_valid_status_code(file_data):
"""Test that exceptions with valid status_code are handled correctly."""
from test.assets.exception_status_code import (
function_raises_exception_with_valid_status_code as test_fn,
)

client = TestClient(wrap_in_fastapi(func=test_fn, plugin_id="mock_plugin"))

post_body = {"file_data": file_data.model_dump()}
resp = client.post("/invoke", json=post_body)
resp_content = resp.json()
invoke_response = InvokeResponse.model_validate(resp_content)

# Should use the exception's status_code
assert invoke_response.status_code == 422
assert "ExceptionWithValidStatusCode" in invoke_response.status_code_text
assert "Test exception with valid status_code" in invoke_response.status_code_text


@pytest.mark.parametrize(
"file_data", mock_file_data, ids=[type(fd).__name__ for fd in mock_file_data]
)
def test_exception_without_status_code(file_data):
"""Test that exceptions without status_code attribute are handled correctly."""
from test.assets.exception_status_code import (
function_raises_exception_without_status_code as test_fn,
)

client = TestClient(wrap_in_fastapi(func=test_fn, plugin_id="mock_plugin"))

post_body = {"file_data": file_data.model_dump()}
resp = client.post("/invoke", json=post_body)
resp_content = resp.json()
invoke_response = InvokeResponse.model_validate(resp_content)

# Should default to 500 when no status_code attribute
assert invoke_response.status_code == 500
assert "ExceptionWithoutStatusCode" in invoke_response.status_code_text
assert "Test exception without status_code" in invoke_response.status_code_text


@pytest.mark.parametrize(
"file_data", mock_file_data, ids=[type(fd).__name__ for fd in mock_file_data]
)
def test_http_exception_handling(file_data):
"""Test that HTTPException is handled correctly (should use HTTPException path)."""
from test.assets.exception_status_code import function_raises_http_exception as test_fn

client = TestClient(wrap_in_fastapi(func=test_fn, plugin_id="mock_plugin"))

post_body = {"file_data": file_data.model_dump()}
resp = client.post("/invoke", json=post_body)
resp_content = resp.json()
invoke_response = InvokeResponse.model_validate(resp_content)

# HTTPException should be handled by the HTTPException handler
assert invoke_response.status_code == 404
assert invoke_response.status_code_text == "Not found"


@pytest.mark.parametrize(
"file_data", mock_file_data, ids=[type(fd).__name__ for fd in mock_file_data]
)
def test_generic_exception_handling(file_data):
"""Test that generic exceptions are handled correctly."""
from test.assets.exception_status_code import function_raises_generic_exception as test_fn

client = TestClient(wrap_in_fastapi(func=test_fn, plugin_id="mock_plugin"))

post_body = {"file_data": file_data.model_dump()}
resp = client.post("/invoke", json=post_body)
resp_content = resp.json()
invoke_response = InvokeResponse.model_validate(resp_content)

# Should default to 500 for generic exceptions
assert invoke_response.status_code == 500
assert "ValueError" in invoke_response.status_code_text
assert "Generic error" in invoke_response.status_code_text


@pytest.mark.parametrize(
"file_data", mock_file_data, ids=[type(fd).__name__ for fd in mock_file_data]
)
def test_async_exception_with_none_status_code(file_data):
"""Test that async functions with status_code=None exceptions are handled correctly."""
from test.assets.exception_status_code import (
async_function_raises_exception_with_none_status_code as test_fn,
)

client = TestClient(wrap_in_fastapi(func=test_fn, plugin_id="mock_plugin"))

post_body = {"file_data": file_data.model_dump()}
resp = client.post("/invoke", json=post_body)
resp_content = resp.json()
invoke_response = InvokeResponse.model_validate(resp_content)

# Should default to 500 when status_code is None
assert invoke_response.status_code == 500
assert "ExceptionWithNoneStatusCode" in invoke_response.status_code_text
assert "Async test exception with None status_code" in invoke_response.status_code_text


def test_streaming_exception_with_none_status_code():
"""Test that async generator functions with
status_code=None exceptions are handled correctly."""
from test.assets.exception_status_code import (
async_gen_function_raises_exception_with_none_status_code as test_fn,
)

client = TestClient(wrap_in_fastapi(func=test_fn, plugin_id="mock_plugin"))

post_body = {"file_data": mock_file_data[0].model_dump()}
resp = client.post("/invoke", json=post_body)

# For streaming responses, we get NDJSON
assert resp.status_code == 200
assert resp.headers["content-type"] == "application/x-ndjson"

# Parse the streaming response - should be a single error response
lines = resp.content.decode().strip().split("\n")
assert len(lines) == 1 # Only error response since no items were yielded

# Parse the error response
import json

error_response = json.loads(lines[0])
invoke_response = InvokeResponse.model_validate(error_response)

# Should default to 500 when status_code is None
assert invoke_response.status_code == 500
assert "ExceptionWithNoneStatusCode" in invoke_response.status_code_text


def test_streaming_exception_with_valid_status_code():
"""Test that async generator functions with
valid status_code exceptions are handled correctly."""
from test.assets.exception_status_code import (
async_gen_function_raises_exception_with_valid_status_code as test_fn,
)

client = TestClient(wrap_in_fastapi(func=test_fn, plugin_id="mock_plugin"))

post_body = {"file_data": mock_file_data[0].model_dump()}
resp = client.post("/invoke", json=post_body)

# For streaming responses, we get NDJSON
assert resp.status_code == 200
assert resp.headers["content-type"] == "application/x-ndjson"

# Parse the streaming response - should be a single error response
lines = resp.content.decode().strip().split("\n")
assert len(lines) == 1 # Only error response since no items were yielded

# Parse the error response
import json

error_response = json.loads(lines[0])
invoke_response = InvokeResponse.model_validate(error_response)

# Should use the exception's status_code
assert invoke_response.status_code == 422
assert "ExceptionWithValidStatusCode" in invoke_response.status_code_text
92 changes: 92 additions & 0 deletions test/assets/exception_status_code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""Test assets for testing exception handling with various status_code scenarios."""

from fastapi import HTTPException


class ExceptionWithNoneStatusCode(Exception):
"""Exception that has status_code attribute set to None."""

def __init__(self, message: str):
super().__init__(message)
self.status_code = None


class ExceptionWithValidStatusCode(Exception):
"""Exception that has status_code attribute set to a valid integer."""

def __init__(self, message: str, status_code: int = 400):
super().__init__(message)
self.status_code = status_code


class ExceptionWithoutStatusCode(Exception):
"""Exception that has no status_code attribute."""

def __init__(self, message: str):
super().__init__(message)


def function_raises_exception_with_none_status_code():
"""Function that raises an exception with status_code=None."""
raise ExceptionWithNoneStatusCode("Test exception with None status_code")


def function_raises_exception_with_valid_status_code():
"""Function that raises an exception with valid status_code."""
raise ExceptionWithValidStatusCode("Test exception with valid status_code", 422)


def function_raises_exception_without_status_code():
"""Function that raises an exception without status_code attribute."""
raise ExceptionWithoutStatusCode("Test exception without status_code")


def function_raises_http_exception():
"""Function that raises FastAPI HTTPException."""
raise HTTPException(status_code=404, detail="Not found")


def function_raises_generic_exception():
"""Function that raises a generic exception."""
raise ValueError("Generic error")


# Async versions for streaming response tests
async def async_function_raises_exception_with_none_status_code():
"""Async function that raises an exception with status_code=None."""
raise ExceptionWithNoneStatusCode("Async test exception with None status_code")


async def async_function_raises_exception_with_valid_status_code():
"""Async function that raises an exception with valid status_code."""
raise ExceptionWithValidStatusCode("Async test exception with valid status_code", 422)


async def async_function_raises_exception_without_status_code():
"""Async function that raises an exception without status_code attribute."""
raise ExceptionWithoutStatusCode("Async test exception without status_code")


# Async generator versions for streaming response error tests
async def async_gen_function_raises_exception_with_none_status_code():
"""Async generator that raises an exception with status_code=None."""
# Don't yield anything, just raise the exception
if False: # This ensures the function is detected as a generator but never yields
yield None
raise ExceptionWithNoneStatusCode("Async gen test exception with None status_code")


async def async_gen_function_raises_exception_with_valid_status_code():
"""Async generator that raises an exception with valid status_code."""
# Don't yield anything, just raise the exception
if False: # This ensures the function is detected as a generator but never yields
yield None
raise ExceptionWithValidStatusCode("Async gen test exception with valid status_code", 422)


async def async_gen_function_raises_exception_without_status_code():
"""Async generator that raises an exception without status_code attribute."""
# Don't yield anything, just raise the exception
if False: # This ensures the function is detected as a generator but never yields
yield None
raise ExceptionWithoutStatusCode("Async gen test exception without status_code")
2 changes: 1 addition & 1 deletion unstructured_platform_plugins/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.0.39" # pragma: no cover
__version__ = "0.0.40" # pragma: no cover
10 changes: 4 additions & 6 deletions unstructured_platform_plugins/etl_uvicorn/api_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,8 @@ async def _stream_response():
filedata_meta=filedata_meta_model.model_validate(
filedata_meta.model_dump()
),
status_code=e.status_code
if hasattr(e, "status_code")
else status.HTTP_500_INTERNAL_SERVER_ERROR,
status_code=getattr(e, "status_code", None)
or status.HTTP_500_INTERNAL_SERVER_ERROR,
status_code_text=f"[{e.__class__.__name__}] {e}",
).model_dump_json()
+ "\n"
Expand Down Expand Up @@ -230,9 +229,8 @@ async def _stream_response():
usage=usage,
message_channels=message_channels,
filedata_meta=filedata_meta_model.model_validate(filedata_meta.model_dump()),
status_code=invoke_error.status_code
if hasattr(invoke_error, "status_code")
else status.HTTP_500_INTERNAL_SERVER_ERROR,
status_code=getattr(invoke_error, "status_code", None)
or status.HTTP_500_INTERNAL_SERVER_ERROR,
status_code_text=f"[{invoke_error.__class__.__name__}] {invoke_error}",
file_data=request_dict.get("file_data", None),
)
Expand Down
Loading