Skip to content

feat: add error handling #47

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
42 changes: 37 additions & 5 deletions src/codeocean/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,62 @@
from dataclasses import dataclass
from requests_toolbelt.adapters.socket_options import TCPKeepAliveAdapter
from requests_toolbelt.sessions import BaseUrlSession
from typing import Optional
from typing import Optional, Union
from urllib3.util import Retry
import requests

from codeocean.capsule import Capsules
from codeocean.computation import Computations
from codeocean.data_asset import DataAssets
from codeocean.errors import (
BadRequestError,
UnauthorizedError,
ForbiddenError,
NotFoundError,
InternalServerError,
CodeOceanError
)


@dataclass
class CodeOcean:

domain: str
token: str
retries: Optional[Retry | int] = 0
retries: Optional[Union[Retry, int]] = 0

def __post_init__(self):
self.session = BaseUrlSession(base_url=f"{self.domain}/api/v1/")
self.session.auth = (self.token, "")
self.session.headers.update({"Content-Type": "application/json"})
self.session.hooks["response"] = [
lambda response, *args, **kwargs: response.raise_for_status()
]
self.session.hooks["response"] = [self.error_handler]
self.session.mount(self.domain, TCPKeepAliveAdapter(max_retries=self.retries))

self.capsules = Capsules(client=self.session)
self.computations = Computations(client=self.session)
self.data_assets = DataAssets(client=self.session)

def error_handler(self, response, *args, **kwargs):
Comment on lines +34 to +41
Copy link
Preview

Copilot AI Jun 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider renaming this internal hook method to _error_handler to signal it's for private use and avoid exposing it as part of the public API.

Suggested change
self.session.hooks["response"] = [self.error_handler]
self.session.mount(self.domain, TCPKeepAliveAdapter(max_retries=self.retries))
self.capsules = Capsules(client=self.session)
self.computations = Computations(client=self.session)
self.data_assets = DataAssets(client=self.session)
def error_handler(self, response, *args, **kwargs):
self.session.hooks["response"] = [self._error_handler]
self.session.mount(self.domain, TCPKeepAliveAdapter(max_retries=self.retries))
self.capsules = Capsules(client=self.session)
self.computations = Computations(client=self.session)
self.data_assets = DataAssets(client=self.session)
def _error_handler(self, response, *args, **kwargs):

Copilot uses AI. Check for mistakes.

try:
response.raise_for_status()
except requests.HTTPError as ex:
try:
error_payload = response.json()
if not isinstance(error_payload, dict):
raise ValueError("Response is not a JSON object")
except ValueError:
error_payload = {"message": response.text or str(ex)}

status_code = response.status_code
if status_code == 400:
raise BadRequestError.from_dict(error_payload).with_error(ex)
elif status_code == 401:
raise UnauthorizedError.from_dict(error_payload).with_error(ex)
elif status_code == 403:
raise ForbiddenError.from_dict(error_payload).with_error(ex)
elif status_code == 404:
raise NotFoundError.from_dict(error_payload).with_error(ex)
elif status_code >= 500:
raise InternalServerError.from_dict(error_payload).with_error(ex)
else:
raise CodeOceanError.from_dict(error_payload).with_error(ex)
63 changes: 63 additions & 0 deletions src/codeocean/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from __future__ import annotations

from dataclasses import dataclass, field
from typing import Optional, List, Type, TypeVar, Dict, Any
import requests

T = TypeVar("T", bound="Error")


@dataclass
class Error(Exception):
message: str
items: Optional[List[str]] = None
error: Optional[requests.HTTPError] = field(default=None, repr=False)

def with_error(self, ex: requests.HTTPError) -> Error:
self.error = ex
return self

@classmethod
def from_dict(cls: Type[T], payload: Dict[str, Any]) -> T:
message = payload.get("message", "An error occurred.")
items = payload.get("items") if isinstance(payload.get("items"), list) else None
return cls(message=message, items=items)

def __str__(self) -> str:
return self.message


@dataclass
class BadRequestError(Error):
"""HTTP 400"""
pass


@dataclass
class UnauthorizedError(Error):
"""HTTP 401"""
pass


@dataclass
class ForbiddenError(Error):
"""HTTP 403"""
pass


@dataclass
class NotFoundError(Error):
"""HTTP 404"""
pass


@dataclass
class InternalServerError(Error):
"""HTTP 5xx"""
pass


@dataclass
class CodeOceanError(Error):
"""Fallback for unexpected errors"""
pass