datamint 2.2.0__tar.gz → 2.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of datamint might be problematic. Click here for more details.

Files changed (50) hide show
  1. {datamint-2.2.0 → datamint-2.3.0}/PKG-INFO +2 -2
  2. {datamint-2.2.0 → datamint-2.3.0}/datamint/api/client.py +13 -5
  3. {datamint-2.2.0 → datamint-2.3.0}/datamint/api/endpoints/__init__.py +3 -1
  4. {datamint-2.2.0 → datamint-2.3.0}/datamint/api/endpoints/annotations_api.py +21 -6
  5. datamint-2.3.0/datamint/api/endpoints/models_api.py +47 -0
  6. {datamint-2.2.0 → datamint-2.3.0}/datamint/api/endpoints/resources_api.py +18 -24
  7. {datamint-2.2.0 → datamint-2.3.0}/datamint/client_cmd_tools/datamint_upload.py +2 -3
  8. {datamint-2.2.0 → datamint-2.3.0}/datamint/exceptions.py +22 -1
  9. {datamint-2.2.0 → datamint-2.3.0}/pyproject.toml +2 -2
  10. {datamint-2.2.0 → datamint-2.3.0}/README.md +0 -0
  11. {datamint-2.2.0 → datamint-2.3.0}/datamint/__init__.py +0 -0
  12. {datamint-2.2.0 → datamint-2.3.0}/datamint/api/__init__.py +0 -0
  13. {datamint-2.2.0 → datamint-2.3.0}/datamint/api/base_api.py +0 -0
  14. {datamint-2.2.0 → datamint-2.3.0}/datamint/api/dto/__init__.py +0 -0
  15. {datamint-2.2.0 → datamint-2.3.0}/datamint/api/endpoints/channels_api.py +0 -0
  16. {datamint-2.2.0 → datamint-2.3.0}/datamint/api/endpoints/datasetsinfo_api.py +0 -0
  17. {datamint-2.2.0 → datamint-2.3.0}/datamint/api/endpoints/projects_api.py +0 -0
  18. {datamint-2.2.0 → datamint-2.3.0}/datamint/api/endpoints/users_api.py +0 -0
  19. {datamint-2.2.0 → datamint-2.3.0}/datamint/api/entity_base_api.py +0 -0
  20. {datamint-2.2.0 → datamint-2.3.0}/datamint/apihandler/annotation_api_handler.py +0 -0
  21. {datamint-2.2.0 → datamint-2.3.0}/datamint/apihandler/api_handler.py +0 -0
  22. {datamint-2.2.0 → datamint-2.3.0}/datamint/apihandler/base_api_handler.py +0 -0
  23. {datamint-2.2.0 → datamint-2.3.0}/datamint/apihandler/dto/__init__.py +0 -0
  24. {datamint-2.2.0 → datamint-2.3.0}/datamint/apihandler/dto/annotation_dto.py +0 -0
  25. {datamint-2.2.0 → datamint-2.3.0}/datamint/apihandler/exp_api_handler.py +0 -0
  26. {datamint-2.2.0 → datamint-2.3.0}/datamint/apihandler/root_api_handler.py +0 -0
  27. {datamint-2.2.0 → datamint-2.3.0}/datamint/client_cmd_tools/__init__.py +0 -0
  28. {datamint-2.2.0 → datamint-2.3.0}/datamint/client_cmd_tools/datamint_config.py +0 -0
  29. {datamint-2.2.0 → datamint-2.3.0}/datamint/configs.py +0 -0
  30. {datamint-2.2.0 → datamint-2.3.0}/datamint/dataset/__init__.py +0 -0
  31. {datamint-2.2.0 → datamint-2.3.0}/datamint/dataset/annotation.py +0 -0
  32. {datamint-2.2.0 → datamint-2.3.0}/datamint/dataset/base_dataset.py +0 -0
  33. {datamint-2.2.0 → datamint-2.3.0}/datamint/dataset/dataset.py +0 -0
  34. {datamint-2.2.0 → datamint-2.3.0}/datamint/entities/__init__.py +0 -0
  35. {datamint-2.2.0 → datamint-2.3.0}/datamint/entities/annotation.py +0 -0
  36. {datamint-2.2.0 → datamint-2.3.0}/datamint/entities/base_entity.py +0 -0
  37. {datamint-2.2.0 → datamint-2.3.0}/datamint/entities/channel.py +0 -0
  38. {datamint-2.2.0 → datamint-2.3.0}/datamint/entities/datasetinfo.py +0 -0
  39. {datamint-2.2.0 → datamint-2.3.0}/datamint/entities/project.py +0 -0
  40. {datamint-2.2.0 → datamint-2.3.0}/datamint/entities/resource.py +0 -0
  41. {datamint-2.2.0 → datamint-2.3.0}/datamint/entities/user.py +0 -0
  42. {datamint-2.2.0 → datamint-2.3.0}/datamint/examples/__init__.py +0 -0
  43. {datamint-2.2.0 → datamint-2.3.0}/datamint/examples/example_projects.py +0 -0
  44. {datamint-2.2.0 → datamint-2.3.0}/datamint/experiment/__init__.py +0 -0
  45. {datamint-2.2.0 → datamint-2.3.0}/datamint/experiment/_patcher.py +0 -0
  46. {datamint-2.2.0 → datamint-2.3.0}/datamint/experiment/experiment.py +0 -0
  47. {datamint-2.2.0 → datamint-2.3.0}/datamint/logging.yaml +0 -0
  48. {datamint-2.2.0 → datamint-2.3.0}/datamint/utils/logging_utils.py +0 -0
  49. {datamint-2.2.0 → datamint-2.3.0}/datamint/utils/torchmetrics.py +0 -0
  50. {datamint-2.2.0 → datamint-2.3.0}/datamint/utils/visualization.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datamint
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: A library for interacting with the Datamint API, designed for efficient data management, processing and Deep Learning workflows.
5
5
  Requires-Python: >=3.10
6
6
  Classifier: Programming Language :: Python :: 3
@@ -21,7 +21,7 @@ Requires-Dist: humanize (>=4.0.0,<5.0.0)
21
21
  Requires-Dist: lazy-loader (>=0.3.0)
22
22
  Requires-Dist: lightning
23
23
  Requires-Dist: matplotlib
24
- Requires-Dist: medimgkit (>=0.7.0)
24
+ Requires-Dist: medimgkit (>=0.7.2)
25
25
  Requires-Dist: nest-asyncio (>=1.0.0,<2.0.0)
26
26
  Requires-Dist: nibabel (>=4.0.0)
27
27
  Requires-Dist: numpy
@@ -1,10 +1,8 @@
1
1
  from typing import Optional
2
- import httpx
3
2
  from .base_api import ApiConfig
4
- from .endpoints import ProjectsApi, ResourcesApi, AnnotationsApi, ChannelsApi, UsersApi, DatasetsInfoApi
3
+ from .endpoints import ProjectsApi, ResourcesApi, AnnotationsApi, ChannelsApi, UsersApi, DatasetsInfoApi, ModelsApi
5
4
  import datamint.configs
6
5
  from datamint.exceptions import DatamintException
7
- import asyncio
8
6
 
9
7
 
10
8
  class Api:
@@ -18,7 +16,8 @@ class Api:
18
16
  'annotations': AnnotationsApi,
19
17
  'channels': ChannelsApi,
20
18
  'users': UsersApi,
21
- 'datasets': DatasetsInfoApi
19
+ 'datasets': DatasetsInfoApi,
20
+ 'models': ModelsApi
22
21
  }
23
22
 
24
23
  def __init__(self,
@@ -73,19 +72,28 @@ class Api:
73
72
  @property
74
73
  def projects(self) -> ProjectsApi:
75
74
  return self._get_endpoint('projects')
75
+
76
76
  @property
77
77
  def resources(self) -> ResourcesApi:
78
78
  return self._get_endpoint('resources')
79
+
79
80
  @property
80
81
  def annotations(self) -> AnnotationsApi:
81
82
  return self._get_endpoint('annotations')
83
+
82
84
  @property
83
85
  def channels(self) -> ChannelsApi:
84
86
  return self._get_endpoint('channels')
87
+
85
88
  @property
86
89
  def users(self) -> UsersApi:
87
90
  return self._get_endpoint('users')
91
+
88
92
  @property
89
93
  def _datasetsinfo(self) -> DatasetsInfoApi:
90
94
  """Internal property to access DatasetsInfoApi."""
91
- return self._get_endpoint('datasets')
95
+ return self._get_endpoint('datasets')
96
+
97
+ @property
98
+ def models(self) -> ModelsApi:
99
+ return self._get_endpoint('models')
@@ -6,6 +6,7 @@ from .projects_api import ProjectsApi
6
6
  from .resources_api import ResourcesApi
7
7
  from .users_api import UsersApi
8
8
  from .datasetsinfo_api import DatasetsInfoApi
9
+ from .models_api import ModelsApi
9
10
 
10
11
  __all__ = [
11
12
  'AnnotationsApi',
@@ -13,5 +14,6 @@ __all__ = [
13
14
  'ProjectsApi',
14
15
  'ResourcesApi',
15
16
  'UsersApi',
16
- 'DatasetsInfoApi'
17
+ 'DatasetsInfoApi',
18
+ 'ModelsApi',
17
19
  ]
@@ -3,9 +3,9 @@ import httpx
3
3
  from datetime import date
4
4
  import logging
5
5
  from ..entity_base_api import ApiConfig, CreatableEntityApi, DeletableEntityApi
6
+ from .models_api import ModelsApi
6
7
  from datamint.entities.annotation import Annotation
7
8
  from datamint.entities.resource import Resource
8
- from datamint.entities.project import Project
9
9
  from datamint.apihandler.dto.annotation_dto import AnnotationType, CreateAnnotationDto, LineGeometry, BoxGeometry, CoordinateSystem, Geometry
10
10
  import numpy as np
11
11
  import os
@@ -38,6 +38,7 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
38
38
  client: Optional HTTP client instance. If None, a new one will be created.
39
39
  """
40
40
  super().__init__(config, Annotation, 'annotations', client)
41
+ self._models_api = ModelsApi(config, client=client)
41
42
 
42
43
  def get_list(self,
43
44
  resource: str | Resource | None = None,
@@ -69,7 +70,7 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
69
70
 
70
71
  async def _upload_segmentations_async(self,
71
72
  resource: str | Resource,
72
- frame_index: int | Sequence [int] | None,
73
+ frame_index: int | Sequence[int] | None,
73
74
  file_path: str | np.ndarray,
74
75
  name: dict[int, str] | dict[tuple, str],
75
76
  imported_from: str | None = None,
@@ -78,7 +79,7 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
78
79
  worklist_id: str | None = None,
79
80
  model_id: str | None = None,
80
81
  transpose_segmentation: bool = False,
81
- upload_volume: bool | str = 'auto'
82
+ upload_volume: bool | str = 'auto',
82
83
  ) -> Sequence[str]:
83
84
  """
84
85
  Upload segmentations asynchronously.
@@ -397,6 +398,7 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
397
398
  worklist_id: str | None = None,
398
399
  model_id: str | None = None,
399
400
  transpose_segmentation: bool = False,
401
+ ai_model_name: str | None = None
400
402
  ) -> list[str]:
401
403
  """
402
404
  Upload segmentations to a resource.
@@ -425,6 +427,7 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
425
427
  worklist_id: The annotation worklist unique id.
426
428
  model_id: The model unique id.
427
429
  transpose_segmentation: Whether to transpose the segmentation or not.
430
+ ai_model_name: Optional AI model name to associate with the segmentation.
428
431
 
429
432
  Returns:
430
433
  List of segmentation unique ids.
@@ -453,6 +456,18 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
453
456
  if isinstance(file_path, str) and not os.path.exists(file_path):
454
457
  raise FileNotFoundError(f"File {file_path} not found.")
455
458
 
459
+ if ai_model_name is not None:
460
+ model_id = self._models_api.get_by_name(ai_model_name)
461
+ if model_id is None:
462
+ try:
463
+ available_models = [model['name'] for model in self._models_api.get_all()]
464
+ except Exception:
465
+ _LOGGER.warning("Could not fetch available AI models from the server.")
466
+ raise ValueError(f"AI model with name '{ai_model_name}' not found. ")
467
+ raise ValueError(f"AI model with name '{ai_model_name}' not found. " +
468
+ f"Available models: {available_models}")
469
+ model_id = model_id['id']
470
+
456
471
  # Handle NIfTI files specially - upload as single volume
457
472
  if isinstance(file_path, str) and (file_path.endswith('.nii') or file_path.endswith('.nii.gz')):
458
473
  _LOGGER.info(f"Uploading NIfTI segmentation file: {file_path}")
@@ -472,7 +487,7 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
472
487
  worklist_id=worklist_id,
473
488
  model_id=model_id,
474
489
  transpose_segmentation=transpose_segmentation,
475
- upload_volume=True
490
+ upload_volume=True,
476
491
  )
477
492
  return loop.run_until_complete(task)
478
493
 
@@ -486,8 +501,8 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
486
501
  raise ValueError("frame_index list contains duplicate values.")
487
502
 
488
503
  if isinstance(frame_index, Sequence) and len(frame_index) == 1:
489
- frame_index = frame_index[0]
490
-
504
+ frame_index = frame_index[0]
505
+
491
506
  nest_asyncio.apply()
492
507
  loop = asyncio.get_event_loop()
493
508
  task = self._upload_segmentations_async(
@@ -0,0 +1,47 @@
1
+ from typing import Sequence
2
+ from ..entity_base_api import ApiConfig, BaseApi
3
+ import httpx
4
+ from datamint.exceptions import EntityAlreadyExistsError
5
+
6
+
7
+ class ModelsApi(BaseApi):
8
+ """API handler for project-related endpoints."""
9
+
10
+ def __init__(self,
11
+ config: ApiConfig,
12
+ client: httpx.Client | None = None) -> None:
13
+ """Initialize the projects API handler.
14
+
15
+ Args:
16
+ config: API configuration containing base URL, API key, etc.
17
+ client: Optional HTTP client instance. If None, a new one will be created.
18
+ """
19
+ super().__init__(config, client=client)
20
+
21
+ def create(self,
22
+ name: str) -> dict:
23
+ json = {
24
+ 'name': name
25
+ }
26
+
27
+ try:
28
+ response = self._make_request('POST',
29
+ 'ai-models',
30
+ json=json)
31
+ return response.json()
32
+ except httpx.HTTPStatusError as e:
33
+ if e.response.status_code == 409:
34
+ raise EntityAlreadyExistsError('ai-model', {'name': name})
35
+ raise
36
+
37
+ def get_all(self) -> Sequence[dict]:
38
+ response = self._make_request('GET',
39
+ 'ai-models')
40
+ return response.json()
41
+
42
+ def get_by_name(self, name: str) -> dict | None:
43
+ models = self.get_all()
44
+ for model in models:
45
+ if model['name'] == name:
46
+ return model
47
+ return None
@@ -85,8 +85,8 @@ class ResourcesApi(CreatableEntityApi[Resource], DeletableEntityApi[Resource]):
85
85
 
86
86
  Args:
87
87
  status: The resource status. Possible values: 'inbox', 'published', 'archived' or None. If None, it will return all resources.
88
- from_date : The start date.
89
- to_date: The end date.
88
+ from_date : The start date (inclusive).
89
+ to_date: The end date (exclusive).
90
90
  tags: The tags to filter the resources.
91
91
  modality: The modality of the resources.
92
92
  mimetype: The mimetype of the resources.
@@ -235,30 +235,26 @@ class ResourcesApi(CreatableEntityApi[Resource], DeletableEntityApi[Resource]):
235
235
  metadata_file: Optional[str | dict] = None,
236
236
  ) -> str:
237
237
  if is_io_object(file_path):
238
- name = file_path.name
238
+ source_filepath = os.path.abspath(os.path.expanduser(file_path.name))
239
+ filename = os.path.basename(source_filepath)
239
240
  else:
240
- name = file_path
241
+ source_filepath = os.path.abspath(os.path.expanduser(file_path))
242
+ filename = os.path.basename(source_filepath)
241
243
 
242
244
  if session is not None and not isinstance(session, aiohttp.ClientSession):
243
245
  raise ValueError("session must be an aiohttp.ClientSession object.")
244
246
 
245
- name = os.path.expanduser(os.path.normpath(name))
246
- if len(Path(name).parts) == 0:
247
- raise ValueError(f"File path '{name}' is not valid.")
248
- name = os.path.join(*[x if x != '..' else '_' for x in Path(name).parts])
249
-
250
247
  if mung_filename is not None:
251
- file_parts = Path(name).parts
248
+ file_parts = Path(source_filepath).parts
252
249
  if file_parts[0] == os.path.sep:
253
250
  file_parts = file_parts[1:]
254
251
  if mung_filename == 'all':
255
- new_file_path = '_'.join(file_parts)
252
+ new_filename = '_'.join(file_parts)
256
253
  else:
257
254
  folder_parts = file_parts[:-1]
258
- new_file_path = '_'.join([folder_parts[i-1] for i in mung_filename if i <= len(folder_parts)])
259
- new_file_path += '_' + file_parts[-1]
260
- name = new_file_path
261
- _LOGGER.debug(f"New file path: {name}")
255
+ new_filename = '_'.join([folder_parts[i-1] for i in mung_filename if i <= len(folder_parts)])
256
+ new_filename += '_' + file_parts[-1]
257
+ filename = new_filename
262
258
 
263
259
  is_a_dicom_file = None
264
260
  if mimetype is None:
@@ -268,14 +264,12 @@ class ResourcesApi(CreatableEntityApi[Resource], DeletableEntityApi[Resource]):
268
264
  mimetype = DEFAULT_NIFTI_MIME
269
265
  break
270
266
  else:
271
- if ext == '.nii.gz' or name.lower().endswith('nii.gz'):
267
+ if ext == '.nii.gz' or filename.lower().endswith('nii.gz'):
272
268
  mimetype = DEFAULT_NIFTI_MIME
273
269
  else:
274
270
  mimetype = mimetype_list[-1] if mimetype_list else DEFAULT_MIME_TYPE
275
271
 
276
272
  mimetype = standardize_mimetype(mimetype)
277
- filename = os.path.basename(name)
278
- _LOGGER.debug(f"File name '{filename}' mimetype: {mimetype}")
279
273
 
280
274
  if is_a_dicom_file == True or is_dicom(file_path):
281
275
  if tags is None:
@@ -292,7 +286,7 @@ class ResourcesApi(CreatableEntityApi[Resource], DeletableEntityApi[Resource]):
292
286
  elif lat == 'R':
293
287
  tags.append("right")
294
288
  # make the dicom `ds` object a file-like object in order to avoid unnecessary disk writes
295
- f = to_bytesio(ds, name)
289
+ f = to_bytesio(ds, filename)
296
290
  else:
297
291
  f = _open_io(file_path)
298
292
 
@@ -329,7 +323,7 @@ class ResourcesApi(CreatableEntityApi[Resource], DeletableEntityApi[Resource]):
329
323
  form.add_field('source', 'api')
330
324
 
331
325
  form.add_field(file_key, f, filename=filename, content_type=mimetype)
332
- form.add_field('source_filepath', name) # full path to the file
326
+ form.add_field('source_filepath', source_filepath) # full path to the file
333
327
  if mimetype is not None:
334
328
  form.add_field('mimetype', mimetype)
335
329
  if channel is not None:
@@ -354,11 +348,11 @@ class ResourcesApi(CreatableEntityApi[Resource], DeletableEntityApi[Resource]):
354
348
  data=form)
355
349
  if 'error' in resp_data:
356
350
  raise DatamintException(resp_data['error'])
357
- _LOGGER.debug(f"Response on uploading {name}: {resp_data}")
351
+ _LOGGER.debug(f"Response on uploading {filename}: {resp_data}")
358
352
  return resp_data['id']
359
353
  except Exception as e:
360
- if 'name' in locals():
361
- _LOGGER.error(f"Error uploading {name}: {e}")
354
+ if 'filename' in locals():
355
+ _LOGGER.error(f"Error uploading {filename}: {e}")
362
356
  else:
363
357
  _LOGGER.error(f"Error uploading {file_path}: {e}")
364
358
  raise
@@ -676,7 +670,7 @@ class ResourcesApi(CreatableEntityApi[Resource], DeletableEntityApi[Resource]):
676
670
  channel='study_channel',
677
671
  segmentation_files={
678
672
  'files': ['path/to/segmentation.nii.gz'],
679
- 'names': {1: 'Bone', 2: 'Tissue'}
673
+ 'names': {1: 'Brain', 2: 'Lung'}
680
674
  },
681
675
  metadata={'patient_age': 45, 'modality': 'CT'}
682
676
  )
@@ -492,7 +492,7 @@ def _get_files_from_path(path: str | Path,
492
492
  Returns:
493
493
  List of file paths as strings
494
494
  """
495
- path = Path(path)
495
+ path = Path(path).resolve()
496
496
 
497
497
  if path.is_file():
498
498
  return [str(path)]
@@ -507,7 +507,7 @@ def _get_files_from_path(path: str | Path,
507
507
 
508
508
  file_paths = walk_to_depth(path, recursive_depth, exclude_pattern)
509
509
  filtered_files = filter_files(file_paths, include_extensions, exclude_extensions)
510
- return [str(f) for f in filtered_files]
510
+ return [str(f.resolve()) for f in filtered_files]
511
511
 
512
512
  except Exception as e:
513
513
  _LOGGER.error(f'Error in recursive search: {e}')
@@ -786,7 +786,6 @@ def main():
786
786
  _USER_LOGGER.error(f'❌ Connection failed: {e}')
787
787
  return
788
788
  try:
789
- print('>>>', segfiles)
790
789
  results = api.resources.upload_resources(channel=args.channel,
791
790
  files_path=files_path,
792
791
  tags=args.tag,
@@ -28,4 +28,25 @@ class ResourceNotFoundError(DatamintException):
28
28
  self.params = params
29
29
 
30
30
  def __str__(self):
31
- return f"Resource '{self.resource_type}' not found for parameters: {self.params}"
31
+ return f"Resource '{self.resource_type}' not found for parameters: {self.params}"
32
+
33
+ # Already existing (e.g, creating a project with a name that already exists)
34
+ class EntityAlreadyExistsError(DatamintException):
35
+ """
36
+ Exception raised when trying to create an entity that already exists.
37
+ For instance, when creating a project with a name that already exists.
38
+ """
39
+
40
+ def __init__(self, entity_type: str, params: dict):
41
+ """Constructor.
42
+
43
+ Args:
44
+ entity_type: The type of entity that already exists.
45
+ params: Dict of params identifying the existing entity.
46
+ """
47
+ super().__init__()
48
+ self.entity_type = entity_type
49
+ self.params = params
50
+
51
+ def __str__(self) -> str:
52
+ return f"Entity '{self.entity_type}' already exists for parameters: {self.params}"
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "datamint"
3
3
  description = "A library for interacting with the Datamint API, designed for efficient data management, processing and Deep Learning workflows."
4
- version = "2.2.0"
4
+ version = "2.3.0"
5
5
  dynamic = ["dependencies"]
6
6
  requires-python = ">=3.10"
7
7
  readme = "README.md"
@@ -40,7 +40,7 @@ matplotlib = "*"
40
40
  lightning = "*"
41
41
  albumentations = ">=2.0.0"
42
42
  lazy-loader = ">=0.3.0"
43
- medimgkit = ">=0.7.0"
43
+ medimgkit = ">=0.7.2"
44
44
  typing_extensions = ">=4.0.0"
45
45
  pydantic = ">=2.6.4"
46
46
  httpx = "*"
File without changes
File without changes
File without changes
File without changes