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
16 changes: 12 additions & 4 deletions src/parxy_core/drivers/abstract_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
FileNotFoundException,
ParsingException,
AuthenticationException,
RateLimitException,
QuotaExceededException,
InputValidationException,
)
from parxy_core.models.config import BaseConfig
from parxy_core.logging import create_null_logger
Expand Down Expand Up @@ -137,9 +140,7 @@ def parse(

except Exception as ex:
self._logger.error(
'Error while parsing file',
file,
self.__class__.__name__,
f'Error while parsing file {file if isinstance(file, str) else "stream"} using {self.__class__.__name__}',
exc_info=True,
)

Expand All @@ -154,7 +155,14 @@ def parse(
parxy_exc = FileNotFoundException(ex, self.__class__)
elif isinstance(
ex,
(FileNotFoundException, AuthenticationException, ParsingException),
(
FileNotFoundException,
AuthenticationException,
ParsingException,
RateLimitException,
QuotaExceededException,
InputValidationException,
),
):
parxy_exc = ex
else:
Expand Down
24 changes: 22 additions & 2 deletions src/parxy_core/drivers/landingai.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
from typing import TYPE_CHECKING

from parxy_core.exceptions.authentication_exception import AuthenticationException
from parxy_core.exceptions.rate_limit_exception import RateLimitException
from parxy_core.exceptions.quota_exceeded_exception import QuotaExceededException
from parxy_core.exceptions.input_validation_exception import InputValidationException
from parxy_core.tracing.utils import trace_with_output

# Type hints that will be available at runtime when unstructured is installed
Expand Down Expand Up @@ -77,7 +80,7 @@ def _handle(
level: str = 'page',
**kwargs,
) -> Document:
from landingai_ade import AuthenticationError
from landingai_ade import AuthenticationError, RateLimitError, APIStatusError

try:
filename, stream = self.handle_file_input(file)
Expand All @@ -90,8 +93,25 @@ def _handle(
except AuthenticationError as aex:
raise AuthenticationException(
message=str(aex),
service=self.__class__,
service=self.__class__.__name__,
) from aex
except RateLimitError as rlex:
raise RateLimitException(
message=str(rlex),
service=self.__class__.__name__,
) from rlex
except APIStatusError as ase:
status_code_exceptions = {
429: RateLimitException,
402: QuotaExceededException,
422: InputValidationException,
}
if exc_class := status_code_exceptions.get(ase.status_code):
raise exc_class(
message=str(ase),
service=self.__class__.__name__,
) from ase
raise

doc = landingaiade_to_parxy(parse_response)

Expand Down
34 changes: 27 additions & 7 deletions src/parxy_core/drivers/llmwhisperer.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
FileNotFoundException,
ParsingException,
AuthenticationException,
RateLimitException,
QuotaExceededException,
InputValidationException,
)
from parxy_core.models import Document, Page
from parxy_core.utils import safe_json_dumps
Expand Down Expand Up @@ -150,19 +153,36 @@ def _handle(
except FileNotFoundError as fex:
raise FileNotFoundException(fex, self.SERVICE_NAME) from fex
except LLMWhispererClientException as wex:
if wex.value['status_code'] in (401, 403):
status_code = wex.value.get('status_code')
error_message = (
str(wex.error_message()) if callable(wex.error_message) else str(wex)
)

if status_code in (401, 403):
raise AuthenticationException(
message=str(wex.error_message()),
message=error_message,
service=self.SERVICE_NAME,
details=wex.value,
) # from wex
else:
raise ParsingException(
wex.error_message if hasattr(wex, 'error_message') else str(wex),
self.SERVICE_NAME,
) from wex

status_code_exceptions = {
429: RateLimitException,
402: QuotaExceededException,
422: InputValidationException,
}
if exc_class := status_code_exceptions.get(status_code):
raise exc_class(
message=error_message,
service=self.SERVICE_NAME,
details=wex.value,
) from wex

raise ParsingException(
error_message,
self.SERVICE_NAME,
details=wex.value,
) from wex

doc = llmwhisperer_to_parxy(res)
doc.filename = filename

Expand Down
9 changes: 9 additions & 0 deletions src/parxy_core/exceptions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,12 @@
from parxy_core.exceptions.unsupported_format_exception import (
UnsupportedFormatException as UnsupportedFormatException,
)
from parxy_core.exceptions.rate_limit_exception import (
RateLimitException as RateLimitException,
)
from parxy_core.exceptions.quota_exceeded_exception import (
QuotaExceededException as QuotaExceededException,
)
from parxy_core.exceptions.input_validation_exception import (
InputValidationException as InputValidationException,
)
66 changes: 66 additions & 0 deletions src/parxy_core/exceptions/input_validation_exception.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from typing import Optional


class InputValidationException(Exception):
"""Exception raised when input fails service validation constraints.

This exception should be raised when a service returns a 422 status code
or indicates that the input doesn't meet requirements such as page limits,
file size limits, or other validation constraints.

Attributes
----------
message : str
Explanation of the validation error
service : str
Name of the service where validation failed (e.g., 'LlamaParse', 'LandingAI')
details : dict, optional
Additional details about the error, such as constraints or limits

Example
---------
try:
# API call fails with 422
raise InputValidationException(
message="PDF must not exceed 100 pages",
service="LandingAI",
details={"max_pages": 100, "actual_pages": 150}
)
except InputValidationException as e:
print(e) # Will print: "Input validation failed for LandingAI: PDF must not exceed 100 pages"
"""

def __init__(
self,
message: str,
service: str,
details: Optional[dict] = None,
):
"""Initialize the input validation error.

Parameters
----------
message : str
Human-readable error message
service : str
Name of the service where validation failed
details : dict, optional
Additional error details, by default None
"""
self.message = message
self.service = service
self.details = details or {}
super().__init__(self.message)

def __str__(self) -> str:
"""Return a string representation of the error.

Returns
-------
str
Formatted error message including service name
"""
base_message = f'Input validation failed for {self.service}: {self.message}'
if self.details:
return f'{base_message}\nDetails: {self.details}'
return base_message
65 changes: 65 additions & 0 deletions src/parxy_core/exceptions/quota_exceeded_exception.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from typing import Optional


class QuotaExceededException(Exception):
"""Exception raised when account quota or balance is insufficient.

This exception should be raised when a service returns a 402 status code
or indicates that the account balance, credits, or quota has been exhausted.

Attributes
----------
message : str
Explanation of the quota error
service : str
Name of the service where quota was exceeded (e.g., 'LlamaParse', 'LandingAI')
details : dict, optional
Additional details about the error, such as response data or remaining quota

Example
---------
try:
# API call fails with 402
raise QuotaExceededException(
message="User balance is insufficient",
service="LandingAI",
details={"error_code": 402, "response": {"error": "Payment Required"}}
)
except QuotaExceededException as e:
print(e) # Will print: "Quota exceeded for LandingAI: User balance is insufficient"
"""

def __init__(
self,
message: str,
service: str,
details: Optional[dict] = None,
):
"""Initialize the quota exceeded error.

Parameters
----------
message : str
Human-readable error message
service : str
Name of the service where quota was exceeded
details : dict, optional
Additional error details, by default None
"""
self.message = message
self.service = service
self.details = details or {}
super().__init__(self.message)

def __str__(self) -> str:
"""Return a string representation of the error.

Returns
-------
str
Formatted error message including service name
"""
base_message = f'Quota exceeded for {self.service}: {self.message}'
if self.details:
return f'{base_message}\nDetails: {self.details}'
return base_message
76 changes: 76 additions & 0 deletions src/parxy_core/exceptions/rate_limit_exception.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
from typing import Optional


class RateLimitException(Exception):
"""Exception raised when API rate limits are exceeded.

This exception should be raised when a service returns a 429 status code
or indicates that the request rate or quota has been exceeded.

Attributes
----------
message : str
Explanation of the rate limit error
service : str
Name of the service where rate limit was hit (e.g., 'LlamaParse', 'LandingAI')
retry_after : int, optional
Number of seconds to wait before retrying, if provided by the service
details : dict, optional
Additional details about the error, such as response data or error codes

Example
---------
try:
# API call fails with 429
raise RateLimitException(
message="Rate limit exceeded",
service="LandingAI",
retry_after=60,
details={"error_code": 429, "response": {"error": "Rate limit exceeded"}}
)
except RateLimitException as e:
print(e) # Will print: "Rate limit exceeded for LandingAI: Rate limit exceeded"
if e.retry_after:
print(f"Retry after {e.retry_after} seconds")
"""

def __init__(
self,
message: str,
service: str,
retry_after: Optional[int] = None,
details: dict = None,
):
"""Initialize the rate limit error.

Parameters
----------
message : str
Human-readable error message
service : str
Name of the service where rate limit was hit
retry_after : int, optional
Seconds to wait before retrying, by default None
details : dict, optional
Additional error details, by default None
"""
self.message = message
self.service = service
self.retry_after = retry_after
self.details = details or {}
super().__init__(self.message)

def __str__(self) -> str:
"""Return a string representation of the error.

Returns
-------
str
Formatted error message including service name and retry info
"""
base_message = f'Rate limit exceeded for {self.service}: {self.message}'
if self.retry_after:
base_message = f'{base_message} (retry after {self.retry_after}s)'
if self.details:
return f'{base_message}\nDetails: {self.details}'
return base_message
Loading