Files
2026-07-13 12:38:34 +08:00

457 lines
11 KiB
Python

"""
Composio exceptions.
"""
import difflib
import typing as t
ENV_COMPOSIO_API_KEY = "COMPOSIO_API_KEY"
class ComposioError(Exception):
"""Base composio SDK error."""
def __init__(
self,
message: str,
*args: t.Any,
delegate: bool = False,
) -> None:
"""
Initialize Composio SDK error.
:param message: Error message
:param delegate: Whether to delegate the error message to the log
collection server or not
"""
super().__init__(message, *args)
self.message = message
self.delegate = delegate
class NotFoundError(ComposioError):
pass
class HTTPError(ComposioError):
"""
Exception class for HTTP API errors.
"""
def __init__(
self,
message: str,
status_code: int,
*args: t.Any,
delegate: bool = False,
) -> None:
"""
Initialize HTTPError class.
:param message: Content from the API response
:param status_code: HTTP response status code
:param delegate: Whether to delegate the error message to the log
collection server or not
"""
super().__init__(message, *args, delegate=delegate)
self.status_code = status_code
class ComposioClientError(ComposioError):
"""
Exception class for Composio client errors.
"""
class SDKError(ComposioError):
pass
class ProcessorError(SDKError):
pass
class EnumError(ComposioError):
pass
class ValidationError(ComposioError):
pass
class JSONSchemaRefResolutionError(ValidationError):
"""Raised when an internal JSON Schema ``$ref`` cannot be inlined.
Covers malformed pointers, missing ``$defs``/``definitions`` targets
(strict mode only), and ``$ref`` chains or node nesting that exceed the
safety caps in :func:`composio.utils.json_schema.dereference_json_schema`.
"""
def __init__(
self,
message: str,
*args: t.Any,
meta: t.Optional[t.Dict[str, t.Any]] = None,
delegate: bool = False,
) -> None:
super().__init__(message, *args, delegate=delegate)
self.meta = meta or {}
class ToolkitError(ComposioError):
pass
class EntityIDError(ComposioError):
pass
class PluginError(ComposioError):
pass
class InvalidParams(ComposioError):
pass
class FileError(ComposioError):
pass
class ComposioSDKTimeoutError(ComposioError, TimeoutError):
pass
class SDKFileNotFoundError(ComposioError, FileNotFoundError):
pass
class LockFileError(ComposioError):
pass
class VersionError(ComposioError):
pass
class InvalidLockFile(LockFileError):
pass
class InvalidVersionString(EnumError):
pass
class VersionSelectionError(LockFileError, VersionError):
def __init__(
self,
action: str,
requested: str,
locked: str,
delegate: bool = False,
) -> None:
self.action = action
self.requested = requested
self.locked = locked
super().__init__(
message=(
f"Error selecting version for action: {action!r}, "
f"requested: {requested!r}, locked: {locked!r}"
),
delegate=delegate,
)
class InvalidEnum(EnumError):
pass
class EnumStringNotFound(EnumError):
"""Raise when user provides invalid enum string."""
def __init__(self, value: str, enum: str, possible_values: t.List[str]) -> None:
error_message = f"Invalid value `{value}` for enum class `{enum}`"
matches = difflib.get_close_matches(value, possible_values, n=1)
if matches:
(match,) = matches
error_message += f". Did you mean {match!r}?"
super().__init__(message=error_message)
class EnumMetadataNotFound(EnumError):
pass
class ErrorUploadingFile(FileError):
pass
class SensitiveFilePathBlockedError(FileError):
"""Raised when a local file path is refused before upload (sensitive directory or credential-like name)."""
class FileUploadPathNotAllowedError(FileError):
"""
Raised when automatic file upload during tool execution is attempted from a
path outside the configured ``file_upload_dirs`` allowlist.
Only fires for auto-upload (enabled via
``dangerously_allow_auto_upload_download_files=True``). Manual upload APIs
are not subject to this check.
"""
class FileUploadAbortedError(FileError):
"""Raised when a ``before_file_upload`` hook returns ``False``."""
class ErrorDownloadingFile(FileError):
pass
class RemoteFileDownloadError(FileError):
"""Raised when fetching a remote file from a tool router session mount fails.
Includes HTTP status, URL, and file path context for debugging.
"""
def __init__(
self,
message: str = "Failed to download remote file",
*,
status_code: t.Optional[int] = None,
status_text: t.Optional[str] = None,
download_url: t.Optional[str] = None,
mount_relative_path: t.Optional[str] = None,
filename: t.Optional[str] = None,
**kwargs: t.Any,
) -> None:
super().__init__(message, **kwargs)
self.status_code = status_code
self.status_text = status_text
self.download_url = download_url
self.mount_relative_path = mount_relative_path
self.filename = filename
class ResponseTooLargeError(FileError):
"""Raised when a response exceeds the maximum allowed size."""
pass
class TriggerError(ToolkitError):
pass
class WebhookSignatureVerificationError(TriggerError):
"""Raised when webhook signature verification fails."""
pass
class WebhookPayloadError(TriggerError):
"""Raised when webhook payload is invalid."""
pass
class TriggerTypeNotFound(TriggerError, NotFoundError):
"""Raised when a trigger type cannot be found for the given slug.
Mirrors the TypeScript SDK's ``ComposioTriggerTypeNotFoundError``.
"""
pass
class ActionError(ToolkitError):
pass
class TriggerSubscriptionError(TriggerError, ComposioClientError):
pass
class InvalidTriggerFilters(TriggerSubscriptionError):
pass
class ApiKeyError(ComposioClientError):
pass
class ApiKeyNotProvidedError(ApiKeyError, NotFoundError):
"""Raise when API key is required but not provided."""
def __init__(self) -> None:
super().__init__(
message=(
"API Key not provided, either provide API key "
f"or export it as `{ENV_COMPOSIO_API_KEY}` "
"or run `composio login`"
),
)
class ResourceError(ComposioClientError):
pass
class NoItemsFound(ResourceError):
"""
Exception class for empty collection values.
"""
class ErrorFetchingResource(ResourceError):
pass
class SchemaError(ToolkitError):
pass
class InvalidSchemaError(SchemaError, TypeError):
pass
class InvalidEntityIdError(EntityIDError, ValueError):
pass
class IntegrationError(ResourceError):
pass
class ConnectedAccountError(ResourceError):
pass
class ConnectedAccountNotFoundError(NotFoundError, ConnectedAccountError):
pass
class InvalidConnectedAccount(ValidationError, ConnectedAccountError):
pass
class ComposioMultipleConnectedAccountsError(ConnectedAccountError):
"""Raised when multiple connected accounts are found for a user and auth config."""
pass
class ComposioLegacyConnectedAccountsEndpointRetiredError(ConnectedAccountError):
"""Raised by ``composio.connected_accounts.initiate()`` when the legacy
``POST /api/v3/connected_accounts`` endpoint rejects a Composio-managed
OAuth (OAuth1, OAuth2, DCR_OAUTH) auth-config request.
Cutover dates: 2026-05-08 (new orgs), 2026-07-03 (all remaining orgs).
Migrate to ``composio.connected_accounts.link()`` — same return shape,
works for every redirectable scheme regardless of whether the auth
config is Composio-managed or custom.
See: https://docs.composio.dev/docs/changelog/2026/04/24
"""
pass
class ComposioAclOnlyForSharedError(ConnectedAccountError):
"""Raised when ACL fields (``allow_all_users``, ``allowed_user_ids``,
``not_allowed_user_ids``) are sent on a ``PRIVATE`` connection. ACL
is only meaningful for ``SHARED`` connections.
Fix: use ``account_type='SHARED'``, or omit the ACL fields when
creating/updating a PRIVATE connection.
"""
pass
class ComposioSharedAccessDeniedError(ConnectedAccountError):
"""Raised when a tool execution attempts to use a SHARED connected
account but the requesting ``user_id`` is not allowed by the
connection's ACL.
Surfaces when a SHARED connection is reached directly (e.g. via
``composio.tools.execute(slug, connected_account_id=...)``) without
going through a tool-router session.
Fix: ask the connection's creator to grant access via
``composio.experimental.update_acl()`` — set ``allow_all_users``,
add the ``user_id`` to ``allowed_user_ids``, or remove it from
``not_allowed_user_ids``.
"""
pass
class ComposioSharedConnectionNotAccessibleError(ConnectedAccountError):
"""Raised by ``tool_router.session.create()`` / ``session.patch()``
when the session's ``user_id`` cannot use a pinned SHARED connection.
Raised at session-create time so the session never enters a state
that fails mid-execution.
Fix: grant the session user access via
``composio.experimental.update_acl()`` on the pinned connection,
or pin a different connection the user can use.
"""
pass
class ErrorProcessingToolExecutionRequest(PluginError):
pass
class DescopeAuthError(ComposioError):
pass
class DescopeConfigError(ComposioError):
pass
class InvalidExecuteFunctionError(ComposioError):
pass
class ToolNotFoundError(ComposioError):
pass
class InvalidModifier(ComposioError):
pass
class ExecuteToolFnNotSetError(ComposioError):
pass
class ToolVersionRequiredError(ComposioError):
"""Raised when toolkit version is not specified for manual tool execution."""
def __init__(self) -> None:
super().__init__(
message=(
"Toolkit version not specified. For manual execution of the tool "
"please pass a specific toolkit version.\n\n"
"Possible fixes:\n"
"1. Pass the toolkit version as a parameter to the execute function "
'("latest" is not supported in manual execution)\n'
"2. Set the toolkit versions in the Composio config "
"(toolkit_versions={'<toolkit-slug>': '<toolkit-version>'})\n"
"3. Set the toolkit version in the environment variable "
"(COMPOSIO_TOOLKIT_VERSION_<TOOLKIT_SLUG>)\n"
"4. Set dangerously_skip_version_check to True "
"(this might cause unexpected behavior when new versions of the tools are released)"
),
)
class UsageError(ComposioError):
pass