Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
cd2107f
Better error handling in BaseApi when streamlining ResourceNotFoundEr…
Lucashsmello Feb 2, 2026
66c8f24
Implement new modular dataset classes: ImageDataset and VolumeDataset…
Lucashsmello Feb 2, 2026
aa34ead
Merge branch 'main' into feat/new-dataset-class
Lucashsmello Feb 2, 2026
40532bd
Refactor AnnotationSetsApi and ProjectsApi to support Project instanc…
Lucashsmello Feb 4, 2026
4279e8c
merge
Lucashsmello Feb 13, 2026
aeef989
Implemented sliced dataset
Lucashsmello Feb 13, 2026
fa6f2c4
Merge branch 'main' into feat/new-dataset-class
Lucashsmello Feb 19, 2026
8508c45
Merge branch 'main' into feat/new-dataset-class
Lucashsmello Feb 23, 2026
050dfeb
Improve axis handling; update dependencies in pyproject.toml
Lucashsmello Feb 23, 2026
e1de013
Merge branch 'main' into feat/new-dataset-class
Lucashsmello Feb 25, 2026
ca7c601
improved slicing logic in SlicedVolumeResource
Lucashsmello Feb 27, 2026
a89ac57
Merge branch 'main' into feat/new-dataset-class
Lucashsmello Mar 4, 2026
5a459ce
Misc organization
Lucashsmello Mar 4, 2026
5a917b5
Little improvement of imports
Lucashsmello Mar 6, 2026
cb3f8a1
Finishing implementation of slicing a VolumeDataset
Lucashsmello Mar 6, 2026
8f37750
Add sliced segmentation caching to SlicedVolumeDataset; refactor Anno…
Lucashsmello Mar 6, 2026
c48f131
Fixed image dimension handling in SlicedVolumeDataset transformation
Lucashsmello Mar 6, 2026
cda8ce0
Small refactor for bettter organization
Lucashsmello Mar 9, 2026
3077b3c
minor refactor
Lucashsmello Mar 9, 2026
a2c4298
VideoDataset class
Lucashsmello Mar 9, 2026
6816cde
Implement dataset splitting and DataModule for Lightning integration;…
Lucashsmello Mar 11, 2026
26157a0
Updated example notebook
Lucashsmello Mar 12, 2026
ac73f9c
Removed deprecated examples
Lucashsmello Mar 12, 2026
3889956
Merge branch 'main' into feat/new-dataset-class
Lucashsmello Mar 13, 2026
120fcd0
Merge branch 'main' into feat/new-dataset-class
Lucashsmello Mar 13, 2026
4eaf414
Add image categories processing and validation strategies to dataset …
Lucashsmello Mar 17, 2026
4a500a2
Add CombinedLoss class to sum multiple loss functions
Lucashsmello Mar 17, 2026
8389beb
Refactor DatamintModel: Extract model lifecycle management to LinkedM…
Lucashsmello Mar 17, 2026
30cd146
Add error handling for empty file content and improve logging in Loca…
Lucashsmello Mar 18, 2026
92187dc
fixed run_id not being passed to log_metrics
Lucashsmello Mar 18, 2026
a0bc8d4
Fixed saving unnecessary transforms as hyperparameters
Lucashsmello Mar 18, 2026
a888765
Refactor tutorial notebook: Update dataset loading section and remove…
Lucashsmello Mar 18, 2026
b423832
Trainer modules
Lucashsmello Mar 18, 2026
48f66ed
Merge branch 'main' into feat/new-dataset-class
Lucashsmello Mar 25, 2026
a2aa892
Refactor model handling and prediction routing in Datamint
Lucashsmello Mar 25, 2026
f923ad1
Enhance annotation handling and inference job predictions; update ver…
Lucashsmello Mar 25, 2026
82cdd59
Add exists_ok parameter to create methods in ProjectsApi and UsersApi…
Lucashsmello Mar 28, 2026
ecc62ea
Enhance InferenceJob and InferenceApi with rich HTML representation a…
Lucashsmello Mar 28, 2026
3567b70
Add tutorial notebook for 2D segmentation using UNet++ on BUSI datase…
Lucashsmello Mar 28, 2026
4a03192
removed unecessary import
Lucashsmello Mar 31, 2026
5eb13e6
Deprecate DatamintBaseDataset and removed hard-coding check of annota…
Lucashsmello Mar 31, 2026
269060f
Disable input example processing and enhance logging in model save fu…
Lucashsmello Mar 31, 2026
867f347
feat: Enhance MLflow integration and per-sample metrics logging
Lucashsmello Mar 31, 2026
e29f310
feat: Improve per-sample metrics logging by batching MLflow requests …
Lucashsmello Mar 31, 2026
16c02fd
feat: Enhance per-sample metrics logging by improving MLflow batch lo…
Lucashsmello Mar 31, 2026
b4b64cf
feat: Add warning for unset MLflow model ID during per-sample metrics…
Lucashsmello Mar 31, 2026
87a37de
added metadata to sample metrics json
Lucashsmello Apr 1, 2026
a7e2634
misc
Lucashsmello Apr 1, 2026
f909e3f
getting metrics after kernel start
Lucashsmello Apr 1, 2026
174bb46
doc
Lucashsmello Apr 3, 2026
9b9973e
feat: Introduce TaskType enumeration for MLflow models and update rel…
Lucashsmello Apr 3, 2026
cef5a66
up version
Lucashsmello Apr 3, 2026
703c4b9
Merge
Lucashsmello Apr 6, 2026
5cb1a65
Merge branch 'main' into feat/new-dataset-class
Lucashsmello Apr 6, 2026
b4517e4
fixed warning
Lucashsmello Apr 6, 2026
9fc772d
Enhance model training and logging:
Lucashsmello Apr 6, 2026
26f0556
Refactor ResourcesApi to allow Project type for publish_to parameter …
Lucashsmello Apr 6, 2026
748036d
tutorial for external model deployment
Lucashsmello Apr 6, 2026
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
14 changes: 11 additions & 3 deletions datamint/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,28 @@
import importlib.metadata
from typing import TYPE_CHECKING
if TYPE_CHECKING:
# Legacy
from .dataset.dataset import DatamintDataset as Dataset
from .apihandler.api_handler import APIHandler

from .api.client import Api
# New modular datasets
from .dataset.image_dataset import ImageDataset
from .dataset.volume_dataset import VolumeDataset

else:
import lazy_loader as lazy

__getattr__, __dir__, __all__ = lazy.attach(
__name__,
submodules=['dataset', "dataset.dataset", "apihandler.api_handler"],
submodules=['dataset', "dataset.dataset"],
submod_attrs={
# Legacy exports
"dataset.dataset": ["DatamintDataset"],
"dataset": ['Dataset'],
"apihandler.api_handler": ["APIHandler"],
"api.client": ["Api"],
# New modular dataset classes
"dataset.image_dataset": ["ImageDataset"],
"dataset.volume_dataset": ["VolumeDataset"],
},
)

Expand Down
16 changes: 7 additions & 9 deletions datamint/api/base_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,10 @@
import json
from PIL import Image
import cv2
import nibabel as nib
from io import BytesIO
import gzip
import contextlib
import asyncio
from medimgkit.format_detection import GZIP_MIME_TYPES, DEFAULT_MIME_TYPE, guess_typez, guess_extension
from datamint.utils.env import ensure_asyncio_loop
import os

Expand Down Expand Up @@ -84,7 +82,6 @@ def __init__(self,
self._aiohttp_connector: aiohttp.TCPConnector | None = None
self._aiohttp_session: aiohttp.ClientSession | None = None
ensure_asyncio_loop()


@staticmethod
def _create_client(config: ApiConfig) -> httpx.Client:
Expand Down Expand Up @@ -435,7 +432,6 @@ def _check_errors_response_httpx(self,
logger.debug("Unable to set message attribute on exception")
pass

logger.error(f"HTTP error {response.status_code} for {url}: {error_msg}")
status_code = response.status_code
if status_code in (400, 404):
new_error_msg = error_msg.replace('404 Not Found', '')
Expand Down Expand Up @@ -687,6 +683,8 @@ def convert_format(bytes_array: bytes,

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

if mimetype is None:
mimetype, ext = BaseApi._determine_mimetype(bytes_array)
Expand Down Expand Up @@ -722,6 +720,8 @@ def convert_format(bytes_array: bytes,
ndata = nib.Nifti1Image.from_stream(f)
ndata.get_fdata() # force loading before IO is closed
return ndata
elif mimetype == 'application/x-empty':
raise ValueError("Empty file content.")

raise ValueError(f"Unsupported mimetype: {mimetype}")

Expand All @@ -732,21 +732,19 @@ def _determine_mimetype(content: bytes,

Args:
content: Raw file content bytes
declared_mimetype: Optional MIME type declared by the source
declared_mimetype: Optional MIME type declared by the source, used as a fallback if content-based detection fails

Returns:
Tuple of (inferred_mimetype, file_extension)
"""
from medimgkit.format_detection import DEFAULT_MIME_TYPE, guess_typez, guess_extension
# Determine mimetype from file content
mimetype_list, ext = guess_typez(content, use_magic=True)
mimetype = mimetype_list[-1]

# get mimetype from resource info if not detected
if declared_mimetype is not None:
if mimetype is None:
mimetype = declared_mimetype
ext = guess_extension(mimetype)
elif mimetype == DEFAULT_MIME_TYPE:
if mimetype is None or mimetype == DEFAULT_MIME_TYPE:
mimetype = declared_mimetype
ext = guess_extension(mimetype)

Expand Down
12 changes: 12 additions & 0 deletions datamint/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,15 @@ def deploy(self) -> DeployModelApi:
def inference(self) -> InferenceApi:
"""Access model inference endpoints."""
return self._get_endpoint('inference', is_mlflow=True)

def __getstate__(self) -> dict:
return {
'server_url': self.config.server_url,
'api_key': self.config.api_key,
'timeout': self.config.timeout,
'max_retries': self.config.max_retries,
'verify_ssl': self.config.verify_ssl,
}

def __setstate__(self, state: dict) -> None:
self.__init__(check_connection=False, **state)
4 changes: 1 addition & 3 deletions datamint/api/dto/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,5 @@
"Geometry",
"BoxGeometry",
"LineGeometry",
"CoordinateSystem"
"LineGeometry",
"CoordinateSystem"
"CoordinateSystem",
]
48 changes: 43 additions & 5 deletions datamint/api/endpoints/annotationsets_api.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,49 @@
from datamint.api.base_api import BaseApi
import logging
from typing import TYPE_CHECKING, Any
from datamint.entities import AnnotationSpec
from collections.abc import Sequence

_LOGGER = logging.getLogger(__name__)
if TYPE_CHECKING:
from datamint.entities import Project


class AnnotationSetsApi(BaseApi):
def get_segmentation_group(self, annotation_set_id: str) -> dict:
"""Get the segmentation group for a given annotation set ID."""
endpoint = f"/annotationsets/{annotation_set_id}/segmentation-group"
ENDPOINT_BASE = "/annotationsets"

def get_segmentation_group(self, annotation_set: 'str | Project') -> dict:
"""Get the segmentation group for a given annotation set ID or Project."""

if isinstance(annotation_set, str):
annotation_set_id = annotation_set
else:
annotation_set_id = annotation_set.worklist_id

endpoint = f"/{self.ENDPOINT_BASE}/{annotation_set_id}/segmentation-group"
return self._make_request("GET", endpoint).json()

def get_annotations_specs(self, annotation_set: 'str | Project') -> Sequence[AnnotationSpec]:
"""Get the annotations specs for a given annotation set ID or Project."""

if isinstance(annotation_set, str):
annotation_set_id = annotation_set
else:
annotation_set_id = annotation_set.worklist_id

result = self._get_by_id(annotation_set_id)

return [AnnotationSpec(**annspec) for annspec in result['annotations']]

def _get_by_id(self, annotation_set_id: str) -> dict[str, Any]:
"""Get an annotation set by its ID.

Args:
annotation_set_id: The ID of the annotation set to retrieve.

Returns:
A dictionary representing the annotation set.
"""
endpoint = f"/{self.ENDPOINT_BASE}/{annotation_set_id}"
result = self._make_request("GET", endpoint).json()

result['annotations'] = [AnnotationSpec(**annspec) for annspec in result['annotations']]
return result
10 changes: 9 additions & 1 deletion datamint/api/endpoints/deploy_model_api.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import httpx

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

Expand All @@ -16,7 +18,13 @@ def get_by_id(self, entity_id: str) -> DeployJob:
data = response.json()
if 'job_id' in data:
data['id'] = data.pop('job_id')
return self._init_entity_obj(**data)
self._validate_uuid(data['id'])
try:
return self._init_entity_obj(**data)
except ResourceNotFoundError as e:
e.resource_type = 'DeployJob'
e.params = {'id': entity_id}
raise

def start(self,
model_name: str,
Expand Down
50 changes: 29 additions & 21 deletions datamint/api/endpoints/inference_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def submit(
model_version: int | None = None,
model_alias: str | None = None,
resource_id: str | None = None,
resource_ids: list[str] | None = None,
# resource_ids: list[str] | None = None,
file_path: str | None = None,
file_paths: list[str] | None = None,
save_results: bool = False,
Expand Down Expand Up @@ -106,8 +106,8 @@ def submit(
save_results=save_results,
params=params,
)
if resource_ids is not None:
payload["resource_ids"] = resource_ids
# if resource_ids is not None:
# payload["resource_ids"] = resource_ids
if file_paths is not None:
payload["file_paths"] = file_paths

Expand Down Expand Up @@ -160,49 +160,55 @@ def wait(
*,
on_status: Callable[[InferenceJob], None] | None = None,
poll_interval: float = 2.0,
timeout: float | None = None,
) -> InferenceJob:
timeout: float | None = 1800,
) -> None:
"""Block until an inference job reaches a terminal state.

First attempts to follow the SSE stream. If the stream is
unavailable or drops early the method falls back to polling
``get_status`` at *poll_interval* seconds.

Args:
job: Job ID string or ``InferenceJob`` entity.
job: Job ID string or ``InferenceJob`` entity. In-place updates to the provided ``InferenceJob`` are made on every status change.
on_status: Optional callback invoked with an updated
``InferenceJob`` each time a status update is received.
poll_interval: Seconds between polls when falling back to
polling mode. Default ``2.0``.
timeout: Maximum seconds to wait. ``None`` means wait
indefinitely. Raises ``TimeoutError`` on expiry.

Returns:
The ``InferenceJob`` in its terminal state.

Raises:
TimeoutError: If *timeout* is set and the job has not
finished within that duration.
"""
job_id = self._entid(job) if not isinstance(job, str) else job
job_id = self._entid(job)
deadline = (time.monotonic() + timeout) if timeout is not None else None

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 TimeoutError(f"Inference job {job_id} did not finish within {timeout}s")

def _notify(event: dict) -> None:
if on_status is None:
return
if isinstance(job, InferenceJob):
# SSE events are partial updates — apply known fields in-place
for key, value in event.items():
try:
setattr(job, key, value)
except Exception:
pass
on_status(job)
else:
on_status(self.get_status(job_id))

# --- Try SSE stream first ---
try:
for event in self.stream_status(job_id):
_check_timeout()
status_str = event.get('status', '')
current_job = self._parse_job_response(event)
if on_status is not None:
on_status(current_job)
if status_str.lower() in _TERMINAL_STATUSES:
return current_job
_notify(event)
if event.get('status', '').lower() in _TERMINAL_STATUSES:
return
except Exception as e:
logger.warning(f"SSE stream ended or failed ({e}); falling back to polling")

Expand All @@ -213,7 +219,7 @@ def _check_timeout() -> None:
if on_status is not None:
on_status(current_job)
if current_job.status.lower() in _TERMINAL_STATUSES:
return current_job
return
time.sleep(poll_interval)

def cancel(self, job: str | InferenceJob) -> bool:
Expand Down Expand Up @@ -392,4 +398,6 @@ def predict_volume(
)
response = self._make_request('POST', f'/{self.endpoint_base}/predict-volume', json=payload)
data = response.json()
return self.get_status(data['job_id'])
return self.get_status(data['job_id'])

predict = submit # Alias for generic prediction endpoint
Loading
Loading