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
52 changes: 33 additions & 19 deletions datamint/api/base_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@
from collections.abc import Generator, AsyncGenerator
import httpx
from dataclasses import dataclass
from datamint.exceptions import DatamintException, ItemNotFoundError
from datamint.exceptions import (
ItemNotFoundError,
AuthenticationError,
PermissionDeniedError,
ValidationError,
NetworkError,
ServerError,
)
import aiohttp
import json
from PIL import Image
Expand Down Expand Up @@ -126,7 +133,7 @@ def _raise_ssl_error(self, original_error: Exception) -> None:
original_error: The original SSL-related exception

Raises:
DatamintException: With helpful troubleshooting information
NetworkError: With helpful troubleshooting information
"""
error_msg = (
f"SSL Certificate verification failed: {original_error}\n\n"
Expand All @@ -141,7 +148,7 @@ def _raise_ssl_error(self, original_error: Exception) -> None:
" api = Api(verify_ssl=False)\n\n"
"For more help, see: https://github.com/SonanceAI/datamint-python-api#-ssl-certificate-troubleshooting"
)
raise DatamintException(error_msg) from original_error
raise NetworkError(error_msg) from original_error

def _create_aiohttp_connector(self, force_close: bool = False) -> aiohttp.TCPConnector:
"""Create aiohttp connector with SSL configuration.
Expand Down Expand Up @@ -417,7 +424,7 @@ def _check_errors_response_httpx(self,
except httpx.ConnectError as e:
if "CERTIFICATE_VERIFY_FAILED" in str(e) or "certificate verify failed" in str(e).lower():
self._raise_ssl_error(e)
raise
raise NetworkError(str(e)) from e
except httpx.HTTPError as e:
try:
response_json = response.json()
Expand All @@ -426,18 +433,21 @@ def _check_errors_response_httpx(self,
error_msg = f"{getattr(e, 'message', str(e))} | {getattr(response, 'text', '')}"
if response_json:
error_msg = f"{error_msg} | {response_json}"
try:
e.message = error_msg
except Exception:
logger.debug("Unable to set message attribute on exception")
pass

status_code = response.status_code
if status_code in (400, 404):
if status_code == 401:
raise AuthenticationError(error_msg) from e
if status_code == 403:
raise PermissionDeniedError(error_msg) from e
if status_code in (400, 422):
raise ValidationError(error_msg) from e
if status_code == 404:
new_error_msg = error_msg.replace('404 Not Found', '')
if ' not found' in new_error_msg.lower() or 'Not Found' in new_error_msg:
# Will be caught by the caller and properly initialized:
raise ItemNotFoundError('unknown', {})
raise ValidationError(error_msg) from e
if status_code >= 500:
raise ServerError(error_msg, status_code=status_code) from e
raise
return response_json

Expand All @@ -447,9 +457,10 @@ async def _check_errors_response_aiohttp(self,
response_json = None
try:
response.raise_for_status()
except aiohttp.ClientConnectionError as e:
raise NetworkError(str(e)) from e
except aiohttp.ClientError as e:
error_msg = str(getattr(e, 'message', e))
# log the raw response for debugging
status_code = BaseApi.get_status_code(e)
# Only read the body on error to get detailed message; do NOT read on success
# as that would exhaust the stream before callers can iterate over it.
Expand All @@ -461,15 +472,18 @@ async def _check_errors_response_aiohttp(self,
error_msg = f"{error_msg} | {response_json}"

logger.error(f"HTTP error {status_code} for {url}: {error_msg}")
try:
e.message = error_msg
except Exception:
logger.debug("Unable to set message attribute on exception")
pass
if status_code in (400, 404):
if status_code == 401:
raise AuthenticationError(error_msg) from e
if status_code == 403:
raise PermissionDeniedError(error_msg) from e
if status_code in (400, 422):
raise ValidationError(error_msg) from e
if status_code == 404:
if ' not found' in error_msg.lower() or 'Not Found' in error_msg:
# Will be caught by the caller and properly initialized:
raise ItemNotFoundError('unknown', {})
raise ValidationError(error_msg) from e
if status_code >= 500:
raise ServerError(error_msg, status_code=status_code) from e
raise
return response_json

Expand Down
8 changes: 4 additions & 4 deletions datamint/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
)
from .endpoints.models_api import ModelsApi
import datamint.configs
from datamint.exceptions import DatamintException
from datamint.exceptions import AuthenticationError, NetworkError
import logging

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -58,7 +58,7 @@ def __init__(self,
if api_key is None:
msg = f"API key not provided! Use the environment variable " + \
f"{Api.DATAMINT_API_VENV_NAME} or pass it as an argument."
raise DatamintException(msg)
raise AuthenticationError(msg)
self.config = ApiConfig(
server_url=server_url,
api_key=api_key,
Expand Down Expand Up @@ -94,8 +94,8 @@ def check_connection(self):
try:
self.projects.get_list(limit=1)
except Exception as e:
raise DatamintException("Error connecting to the Datamint API." +
f" Please check your api_key and/or other configurations.") from e
raise NetworkError("Error connecting to the Datamint API."
" Please check your api_key and/or other configurations.") from e

def close(self) -> None:
"""Close underlying HTTP clients and any shared aiohttp sessions.
Expand Down
22 changes: 11 additions & 11 deletions datamint/api/endpoints/annotations_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
LineAnnotation,
annotation_from_dict,
)
from datamint.exceptions import DatamintException, ItemNotFoundError
from datamint.exceptions import ItemNotFoundError, ServerError
from datamint.utils.nifti_utils import metadata_to_nifti_obj

from ..entity_base_api import ApiConfig, CreatableEntityApi, DeletableEntityApi
Expand Down Expand Up @@ -453,7 +453,7 @@ async def upload_annotation_file_async(self,
the basename of the path will be used.

Raises:
DatamintException: If the upload fails.
ServerError: If the upload fails.

"""
f, filename, close_file, content_type = self._prepare_upload_file(file,
Expand All @@ -470,7 +470,7 @@ async def upload_annotation_file_async(self,
session=session,
data=form)
if isinstance(respdata, dict) and 'error' in respdata:
raise DatamintException(respdata['error'])
raise ServerError(respdata['error'])
finally:
if close_file:
f.close()
Expand All @@ -494,7 +494,7 @@ def upload_annotation_file(self,
the basename of the path will be used.

Raises:
DatamintException: If the upload fails.
ServerError: If the upload fails.
"""
f, filename, close_file, content_type = self._prepare_upload_file(file,
filename,
Expand All @@ -509,7 +509,7 @@ def upload_annotation_file(self,
files=files)
respdata = resp.json()
if isinstance(respdata, dict) and 'error' in respdata:
raise DatamintException(respdata['error'])
raise ServerError(respdata['error'])
finally:
if close_file:
f.close()
Expand Down Expand Up @@ -550,7 +550,7 @@ def create(
json=annotations_payload).json()
for r in respdata:
if isinstance(r, dict) and 'error' in r:
raise DatamintException(r['error'])
raise ServerError(r['error'])
if is_single_annotation:
return respdata[0]
return respdata
Expand Down Expand Up @@ -788,7 +788,7 @@ async def _create_annotations_async(self,
json=annotations)
for r in respdata:
if isinstance(r, dict) and 'error' in r:
raise DatamintException(r['error'])
raise ServerError(r['error'])
return respdata

@staticmethod
Expand Down Expand Up @@ -917,7 +917,7 @@ async def _upload_volume_segmentation_async(self,
raise

if 'error' in respdata:
raise DatamintException(respdata['error'])
raise ServerError(respdata['error'])
return respdata
else:
raise ValueError(f"Volume upload not supported for file format: {file_path}")
Expand Down Expand Up @@ -951,7 +951,7 @@ async def _upload_volume_segmentation_async(self,
raise

if 'error' in respdata:
raise DatamintException(respdata['error'])
raise ServerError(respdata['error'])
return respdata
else:
raise ValueError(f"Unsupported file_path type for volume upload: {type(file_path)}")
Expand Down Expand Up @@ -1460,7 +1460,7 @@ def patch(self,
project_id: Optional project ID to associate with the annotation.

Raises:
DatamintException: If the update fails.
ServerError: If the update fails.
"""
annotation_id = self._entid(annotation)

Expand All @@ -1477,7 +1477,7 @@ def patch(self,

respdata = resp.json()
if isinstance(respdata, dict) and 'error' in respdata:
raise DatamintException(respdata['error'])
raise ServerError(respdata['error'])

def approve(self, annotation: str | Annotation) -> None:
"""Approve an annotation.
Expand Down
4 changes: 2 additions & 2 deletions datamint/api/endpoints/deploy_model_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import httpx

from datamint.exceptions import ResourceNotFoundError
from datamint.exceptions import ResourceNotFoundError, JobTimeoutError
from ..entity_base_api import EntityBaseApi, ApiConfig
from datamint.entities.deployjob import DeployJob

Expand Down Expand Up @@ -88,7 +88,7 @@ def wait(

def _check_timeout() -> None:
if deadline is not None and time.monotonic() >= deadline:
raise TimeoutError(f"Deployment job {job_id} did not finish within {timeout}s")
raise JobTimeoutError(f"Deployment job {job_id} did not finish within {timeout}s")

def _notify(event: dict) -> None:
if on_status is None:
Expand Down
3 changes: 2 additions & 1 deletion datamint/api/endpoints/inference_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from ..entity_base_api import EntityBaseApi, ApiConfig
from datamint.entities.inferencejob import InferenceJob
from datamint.exceptions import JobTimeoutError

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -186,7 +187,7 @@ def wait(

def _check_timeout() -> None:
if deadline is not None and time.monotonic() >= deadline:
raise TimeoutError(f"Inference job {job_id} did not finish within {timeout}s")
raise JobTimeoutError(f"Inference job {job_id} did not finish within {timeout}s")

def _notify(event: dict) -> None:
if on_status is None:
Expand Down
2 changes: 1 addition & 1 deletion datamint/api/endpoints/projects_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ def add_resources(self,
# get the project id by its name
project_found = self._get_by_name_or_id(project)
if project_found is None:
raise ValueError(f"Project '{project}' not found.")
raise ItemNotFoundError('Project', {'name': project})
project_id = project_found.id
else:
project_id = project.id
Expand Down
19 changes: 10 additions & 9 deletions datamint/api/endpoints/resources_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from ..entity_base_api import CreatableEntityApi, DeletableEntityApi
from datamint.entities import Project, Resource
from datamint.entities.annotations.annotation import Annotation
from datamint.exceptions import DatamintException, ItemNotFoundError
from datamint.exceptions import ItemNotFoundError, ServerError, ValidationError
from datamint.entities.annotations import AnnotationType
from datamint.utils.collection_utils import ChainedSequence
import httpx
Expand Down Expand Up @@ -431,7 +431,7 @@ async def _upload_single_resource_async(self,
session=session,
timeout=timeout)
if 'error' in resp_data:
raise DatamintException(resp_data['error'])
raise ServerError(resp_data['error'])
_LOGGER.debug("Response on uploading %s: %s", filename, resp_data)
return resp_data['id']
except Exception as e:
Expand Down Expand Up @@ -845,7 +845,7 @@ def upload_resource(self,

Raises:
ItemNotFoundError: If `publish_to` is supplied, and the project does not exist.
DatamintException: If the upload fails.
ServerError: If the upload fails.

Example:
.. code-block:: python
Expand Down Expand Up @@ -902,7 +902,7 @@ def upload_resource(self,
return r
else:
# This should not happen with single file uploads, but handle it just in case
raise DatamintException(f"Unexpected return from upload_resources: {type(result)} | {result}")
raise ServerError(f"Unexpected return from upload_resources: {type(result)} | {result}")

async def _async_download_file(self,
resource: str | Resource,
Expand Down Expand Up @@ -1211,16 +1211,16 @@ def download_resource_frame(self,

Raises:
ItemNotFoundError: If the resource does not exists.
DatamintException: If the resource is not a video or dicom.
ServerError: If the resource is not a video or dicom.
"""
# check if the resource is an single frame image (png,jpeg,...) first.
# If so, download the whole resource file and return the image.
if not isinstance(resource, Resource):
resource = self.get_by_id(resource)
if resource.is_image():
if frame_index != 0:
raise DatamintException(f"Resource {resource.id} is not a multi-frame resource, "
f"but frame_index is {frame_index}.")
raise ValidationError(f"Resource {resource.id} is not a multi-frame resource, "
f"but frame_index is {frame_index}.")
return self.download_resource_file(resource, auto_convert=True)

try:
Expand All @@ -1231,8 +1231,9 @@ def download_resource_frame(self,
if response.status_code == 200:
return Image.open(io.BytesIO(response.content))
else:
raise DatamintException(
f"Error downloading frame {frame_index} of resource {self._entid(resource)}: {response.text}")
raise ServerError(
f"Error downloading frame {frame_index} of resource {self._entid(resource)}: {response.text}",
status_code=response.status_code)
except ItemNotFoundError as e:
e.set_params('resource', {'resource_id': self._entid(resource)})
raise e
Expand Down
4 changes: 2 additions & 2 deletions datamint/api/entity_base_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import logging
import httpx
from datamint.entities.base_entity import BaseEntity
from datamint.exceptions import DatamintException, ItemNotFoundError
from datamint.exceptions import ItemNotFoundError, ServerError
import aiohttp
import asyncio
from .base_api import ApiConfig, BaseApi
Expand Down Expand Up @@ -216,7 +216,7 @@ async def _create_async(self, entity_data: dict[str, Any]) -> str | Sequence[str
f'/{self.endpoint_base}',
json=entity_data)
if 'error' in respdata:
raise DatamintException(respdata['error'])
raise ServerError(respdata['error'])
if isinstance(respdata, str):
return respdata
if isinstance(respdata, list):
Expand Down
4 changes: 2 additions & 2 deletions datamint/dataset/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from torch.utils.data import DataLoader, ConcatDataset
import numpy as np
from datamint.entities.annotation_worklist import AnnotationWorklist
from datamint.exceptions import DatamintException
from datamint.exceptions import DatamintException, ItemNotFoundError
from .annotation_processor import AnnotationProcessor, MergeStrategy
from datamint.entities.annotations.annotation_spec import AnnotationSpec, CategoryAnnotationSpec
from datamint.entities.annotations import AnnotationType
Expand Down Expand Up @@ -451,7 +451,7 @@ def _initialize_from_project(
if isinstance(project, str):
project = self._api.projects.get_by_name(project)
if project is None:
raise DatamintDatasetException(f"Project '{project}' not found.")
raise ItemNotFoundError('Project', {'name': project})
else:
# Attach API to project if not already set
if not hasattr(project, '_api') or project._api is None:
Expand Down
Loading
Loading