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
29 changes: 29 additions & 0 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Lint

on:
push:
branches: [main]
pull_request:
branches: [main, fix/*, hotfix/*, release/*, develop]

permissions:
contents: read

jobs:
ruff:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install ruff
run: pip install "ruff==0.16.1"

- name: Run ruff check
run: ruff check .
continue-on-error: true
17 changes: 12 additions & 5 deletions datamint/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,23 @@

import importlib.metadata
from typing import TYPE_CHECKING

from .utils.logging_utils import setup_file_logging_if_enabled

setup_file_logging_if_enabled()
if TYPE_CHECKING:
from .api.client import Api
from .api.client import Api as Api

# New modular datasets
from .dataset.image_dataset import ImageDataset
from .dataset.volume_dataset import VolumeDataset
from .mlflow.flavors.validation import validate_model, ValidationReport, ValidationIssue, ModelValidationError
from .default_project import select_project
from .dataset.image_dataset import ImageDataset as ImageDataset
from .dataset.volume_dataset import VolumeDataset as VolumeDataset
from .default_project import select_project as select_project
from .mlflow.flavors.validation import (
ModelValidationError as ModelValidationError,
ValidationIssue as ValidationIssue,
ValidationReport as ValidationReport,
validate_model as validate_model,
)

else:
import lazy_loader as lazy
Expand Down
53 changes: 31 additions & 22 deletions datamint/api/base_api.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
import asyncio
import contextlib
import gzip
import json
import logging
import os
from collections.abc import AsyncGenerator, Generator
from dataclasses import dataclass
from io import BytesIO
from typing import TYPE_CHECKING
from collections.abc import Generator, AsyncGenerator

import aiohttp
import cv2
import httpx
from dataclasses import dataclass
from PIL import Image

from datamint.exceptions import (
ItemNotFoundError,
AuthenticationError,
PermissionDeniedError,
ValidationError,
ItemNotFoundError,
NetworkError,
PermissionDeniedError,
ServerError,
ValidationError,
)
import aiohttp
import json
from PIL import Image
import cv2
from io import BytesIO
import gzip
import contextlib
import asyncio
from datamint.utils.env import ensure_asyncio_loop
import os

if TYPE_CHECKING:
from datamint.api.client import Api
Expand Down Expand Up @@ -86,9 +88,10 @@ def __init__(self,
self._pid = os.getpid() # Track PID to detect DataLoader worker forks
self.client = client or BaseApi._create_client(config)
self.semaphore = asyncio.Semaphore(_ASYNC_REQUEST_LIMIT)
self._api_instance: 'Api | None' = None # Injected by Api class
self._api_instance: Api | None = None # Injected by Api class
self._aiohttp_connector: aiohttp.TCPConnector | None = None
self._aiohttp_session: aiohttp.ClientSession | None = None
self._pending_close_task: asyncio.Task | None = None
ensure_asyncio_loop()

@staticmethod
Expand Down Expand Up @@ -160,6 +163,7 @@ def _create_aiohttp_connector(self, force_close: bool = False) -> aiohttp.TCPCon
Configured TCPConnector for aiohttp sessions.
"""
import ssl

import certifi

limit = _ASYNC_REQUEST_LIMIT
Expand Down Expand Up @@ -215,10 +219,10 @@ def _close_aiohttp_session(self) -> None:
# If we're in an environment where the loop is running and not patched,
# fall back to scheduling the close.
try:
loop.create_task(self._aiohttp_session.close())
self._pending_close_task = loop.create_task(self._aiohttp_session.close())
self._pending_close_task.add_done_callback(lambda _: setattr(self, '_pending_close_task', None))
except Exception as e:
logger.info(f"Unable to schedule aiohttp session close: {e}")
pass
finally:
self._aiohttp_session = None
self._aiohttp_connector = None
Expand Down Expand Up @@ -305,6 +309,7 @@ def _ensure_client_fresh(self) -> None:
# Invalidate any inherited aiohttp session as well.
self._aiohttp_session = None
self._aiohttp_connector = None
self._pending_close_task = None

def _make_request(self, method: str, endpoint: str, **kwargs) -> httpx.Response:
"""Make HTTP request with error handling and retries.
Expand Down Expand Up @@ -699,12 +704,12 @@ def convert_format(bytes_array: bytes,
>>> dicom = BaseApi.convert_format(dicom_bytes)

"""
import pydicom
import nibabel as nib
import pydicom
from medimgkit.format_detection import GZIP_MIME_TYPES

if mimetype is None:
mimetype, ext = BaseApi._determine_mimetype(bytes_array)
mimetype, _ext = BaseApi._determine_mimetype(bytes_array)
if mimetype is None:
raise ValueError("Could not determine mimetype from content.")
content_io = BytesIO(bytes_array)
Expand All @@ -725,12 +730,12 @@ def convert_format(bytes_array: bytes,
ndata = nib.Nifti1Image.from_stream(content_io)
ndata.get_fdata() # force loading before IO is closed
return ndata
except Exception as e:
except Exception:
if file_path is not None:
ndata = nib.load(file_path)
ndata.get_fdata() # force loading before IO is closed
return ndata
raise e
raise
elif mimetype in GZIP_MIME_TYPES:
# let's hope it's a .nii.gz
with gzip.open(content_io, 'rb') as f:
Expand All @@ -754,7 +759,11 @@ def _determine_mimetype(content: bytes,
Returns:
Tuple of (inferred_mimetype, file_extension)
"""
from medimgkit.format_detection import DEFAULT_MIME_TYPE, guess_typez, guess_extension
from medimgkit.format_detection import (
DEFAULT_MIME_TYPE,
guess_extension,
guess_typez,
)
# Determine mimetype from file content
mimetype_list, ext = guess_typez(content, use_magic=True)
mimetype = mimetype_list[-1]
Expand Down
32 changes: 19 additions & 13 deletions datamint/api/client.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
from typing import Any
import logging
from typing import Any, ClassVar

from .base_api import ApiConfig, BaseApi
from .endpoints import (ProjectsApi, ResourcesApi, AnnotationsApi,
ChannelsApi, UsersApi, DatasetsInfoApi,
AnnotationWorklistApi, DeployModelApi,
InferenceApi
)
from .endpoints.models_api import ModelsApi
import datamint.configs
from datamint.exceptions import AuthenticationError, NetworkError
import logging

from .base_api import ApiConfig, BaseApi
from .endpoints import (
AnnotationsApi,
AnnotationWorklistApi,
ChannelsApi,
DatasetsInfoApi,
DeployModelApi,
InferenceApi,
ProjectsApi,
ResourcesApi,
UsersApi,
)
from .endpoints.models_api import ModelsApi

_LOGGER = logging.getLogger(__name__)

Expand All @@ -19,7 +26,7 @@ class Api:
DEFAULT_SERVER_URL = 'https://api.datamint.io'
DATAMINT_API_VENV_NAME = datamint.configs.ENV_VARS[datamint.configs.APIKEY_KEY]

_API_MAP: dict[str, type[BaseApi]] = {
_API_MAP: ClassVar[dict[str, type[BaseApi]]] = {
'projects': ProjectsApi,
'resources': ResourcesApi,
'annotations': AnnotationsApi,
Expand All @@ -37,7 +44,7 @@ class Api:
# (e.g. one per DataLoader worker or dataset auto-refresh) once a given
# configuration is known to work. A changed value simply misses the cache,
# forcing a fresh check.
_verified_connections: set[tuple[str, str | None, bool | str]] = set()
_verified_connections: ClassVar[set[tuple[str, str | None, bool | str]]] = set()

def __init__(self,
server_url: str | None = None,
Expand Down Expand Up @@ -67,7 +74,7 @@ def __init__(self,
if api_key is None:
api_key = datamint.configs.get_value(datamint.configs.APIKEY_KEY)
if api_key is None:
msg = f"API key not provided! Use the environment variable " + \
msg = "API key not provided! Use the environment variable " + \
f"{Api.DATAMINT_API_VENV_NAME} or pass it as an argument."
raise AuthenticationError(msg)
self.config = ApiConfig(
Expand Down Expand Up @@ -121,7 +128,6 @@ def close(self) -> None:
endpoint.close()
except Exception as e:
_LOGGER.warning(f"Error closing endpoint {endpoint}: {e}")
pass

# Close shared httpx clients owned by this Api
for client in (self._client, self._highclient, self._mlclient):
Expand Down
3 changes: 1 addition & 2 deletions datamint/api/dto/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
CreateAnnotationDto,
)


__all__ = [
"annotation_dto",
"CreateAnnotationDto",
"annotation_dto",
]
5 changes: 3 additions & 2 deletions datamint/api/dto/annotation_dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
CreateAnnotationDto: Main DTO for creating annotation requests.
"""

from typing import Any, TYPE_CHECKING
from typing import TYPE_CHECKING, Any

from datamint.entities.annotations import AnnotationType

if TYPE_CHECKING:
Expand Down Expand Up @@ -50,7 +51,7 @@ def __init__(self,
self.units = units
self.model_id = model_id
if model_id is not None:
if is_model == False:
if is_model is False:
raise ValueError("model_id==False while self.model_id is provided.")
if not isinstance(model_id, str):
raise ValueError("model_id must be a string if provided.")
Expand Down
18 changes: 9 additions & 9 deletions datamint/api/endpoints/__init__.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
"""API endpoint handlers."""

from .annotations_api import AnnotationsApi
from .annotationsets_api import AnnotationWorklistApi
from .channels_api import ChannelsApi
from .projects_api import ProjectsApi
from .resources_api import ResourcesApi
from .users_api import UsersApi
from .datasetsinfo_api import DatasetsInfoApi
from .annotationsets_api import AnnotationWorklistApi
from .deploy_model_api import DeployModelApi
from .inference_api import InferenceApi
from .projects_api import ProjectsApi
from .resources_api import ResourcesApi
from .users_api import UsersApi

__all__ = [
'AnnotationWorklistApi',
'AnnotationsApi',
'ChannelsApi',
'ProjectsApi',
'ResourcesApi',
'UsersApi',
'ChannelsApi',
'DatasetsInfoApi',
'AnnotationWorklistApi',
'DeployModelApi',
'InferenceApi',
'ProjectsApi',
'ResourcesApi',
'UsersApi',
]
Loading
Loading