datamint 2.3.3__py3-none-any.whl → 2.9.0__py3-none-any.whl

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.
Files changed (59) hide show
  1. datamint/__init__.py +1 -3
  2. datamint/api/__init__.py +0 -3
  3. datamint/api/base_api.py +286 -54
  4. datamint/api/client.py +76 -13
  5. datamint/api/endpoints/__init__.py +2 -2
  6. datamint/api/endpoints/annotations_api.py +186 -28
  7. datamint/api/endpoints/deploy_model_api.py +78 -0
  8. datamint/api/endpoints/models_api.py +1 -0
  9. datamint/api/endpoints/projects_api.py +38 -7
  10. datamint/api/endpoints/resources_api.py +227 -100
  11. datamint/api/entity_base_api.py +66 -7
  12. datamint/apihandler/base_api_handler.py +0 -1
  13. datamint/apihandler/dto/annotation_dto.py +2 -0
  14. datamint/client_cmd_tools/datamint_config.py +0 -1
  15. datamint/client_cmd_tools/datamint_upload.py +3 -1
  16. datamint/configs.py +11 -7
  17. datamint/dataset/base_dataset.py +24 -4
  18. datamint/dataset/dataset.py +1 -1
  19. datamint/entities/__init__.py +1 -1
  20. datamint/entities/annotations/__init__.py +13 -0
  21. datamint/entities/{annotation.py → annotations/annotation.py} +81 -47
  22. datamint/entities/annotations/image_classification.py +12 -0
  23. datamint/entities/annotations/image_segmentation.py +252 -0
  24. datamint/entities/annotations/volume_segmentation.py +273 -0
  25. datamint/entities/base_entity.py +100 -6
  26. datamint/entities/cache_manager.py +129 -15
  27. datamint/entities/datasetinfo.py +60 -65
  28. datamint/entities/deployjob.py +18 -0
  29. datamint/entities/project.py +39 -0
  30. datamint/entities/resource.py +310 -46
  31. datamint/lightning/__init__.py +1 -0
  32. datamint/lightning/datamintdatamodule.py +103 -0
  33. datamint/mlflow/__init__.py +65 -0
  34. datamint/mlflow/artifact/__init__.py +1 -0
  35. datamint/mlflow/artifact/datamint_artifacts_repo.py +8 -0
  36. datamint/mlflow/env_utils.py +131 -0
  37. datamint/mlflow/env_vars.py +5 -0
  38. datamint/mlflow/flavors/__init__.py +17 -0
  39. datamint/mlflow/flavors/datamint_flavor.py +150 -0
  40. datamint/mlflow/flavors/model.py +877 -0
  41. datamint/mlflow/lightning/callbacks/__init__.py +1 -0
  42. datamint/mlflow/lightning/callbacks/modelcheckpoint.py +410 -0
  43. datamint/mlflow/models/__init__.py +93 -0
  44. datamint/mlflow/tracking/datamint_store.py +76 -0
  45. datamint/mlflow/tracking/default_experiment.py +27 -0
  46. datamint/mlflow/tracking/fluent.py +91 -0
  47. datamint/utils/env.py +27 -0
  48. datamint/utils/visualization.py +21 -13
  49. datamint-2.9.0.dist-info/METADATA +220 -0
  50. datamint-2.9.0.dist-info/RECORD +73 -0
  51. {datamint-2.3.3.dist-info → datamint-2.9.0.dist-info}/WHEEL +1 -1
  52. datamint-2.9.0.dist-info/entry_points.txt +18 -0
  53. datamint/apihandler/exp_api_handler.py +0 -204
  54. datamint/experiment/__init__.py +0 -1
  55. datamint/experiment/_patcher.py +0 -570
  56. datamint/experiment/experiment.py +0 -1049
  57. datamint-2.3.3.dist-info/METADATA +0 -125
  58. datamint-2.3.3.dist-info/RECORD +0 -54
  59. datamint-2.3.3.dist-info/entry_points.txt +0 -4
@@ -1,4 +1,5 @@
1
- from typing import Any, TypeVar, Generic, Type, Sequence
1
+ from typing import Any, Literal, TypeVar, Generic, overload
2
+ from collections.abc import Sequence, AsyncGenerator
2
3
  import logging
3
4
  import httpx
4
5
  from datamint.entities.base_entity import BaseEntity
@@ -7,7 +8,6 @@ import aiohttp
7
8
  import asyncio
8
9
  from .base_api import ApiConfig, BaseApi
9
10
  import contextlib
10
- from typing import AsyncGenerator
11
11
 
12
12
  logger = logging.getLogger(__name__)
13
13
  T = TypeVar('T', bound=BaseEntity)
@@ -24,9 +24,10 @@ class EntityBaseApi(BaseApi, Generic[T]):
24
24
  """
25
25
 
26
26
  def __init__(self, config: ApiConfig,
27
- entity_class: Type[T],
27
+ entity_class: type[T],
28
28
  endpoint_base: str,
29
- client: httpx.Client | None = None) -> None:
29
+ client: httpx.Client | None = None
30
+ ) -> None:
30
31
  """Initialize the entity API handler.
31
32
 
32
33
  Args:
@@ -53,6 +54,11 @@ class EntityBaseApi(BaseApi, Generic[T]):
53
54
  entity_id: str | BaseEntity,
54
55
  add_path: str = '',
55
56
  **kwargs) -> httpx.Response:
57
+ """
58
+ Make an HTTP request for a specific entity by its ID.
59
+ It is basically a wrapper around :py:meth:`_make_request` that
60
+ constructs the URL for the entity in this form: `/{endpoint_base}/{entity_id}/{add_path}`
61
+ """
56
62
  try:
57
63
  entity_id = self._entid(entity_id)
58
64
  add_path = '/'.join(add_path.strip().strip('/').split('/'))
@@ -61,6 +67,10 @@ class EntityBaseApi(BaseApi, Generic[T]):
61
67
  if e.response.status_code == 404:
62
68
  raise ResourceNotFoundError(self.endpoint_base, {'id': entity_id}) from e
63
69
  raise
70
+ except ResourceNotFoundError as e:
71
+ e.resource_type = self.endpoint_base
72
+ e.params = {'id': entity_id}
73
+ raise
64
74
 
65
75
  @contextlib.asynccontextmanager
66
76
  async def _make_entity_request_async(self,
@@ -81,6 +91,23 @@ class EntityBaseApi(BaseApi, Generic[T]):
81
91
  if e.status == 404:
82
92
  raise ResourceNotFoundError(self.endpoint_base, {'id': entity_id}) from e
83
93
  raise
94
+ except ResourceNotFoundError as e:
95
+ e.resource_type = self.endpoint_base
96
+ e.params = {'id': entity_id}
97
+ raise
98
+
99
+ async def _make_entity_request_async_json(self,
100
+ method: str,
101
+ entity_id: str | BaseEntity,
102
+ add_path: str = '',
103
+ session: aiohttp.ClientSession | None = None,
104
+ **kwargs):
105
+ async with self._make_entity_request_async(method,
106
+ entity_id,
107
+ add_path=add_path,
108
+ session=session,
109
+ **kwargs) as resp:
110
+ return await resp.json()
84
111
 
85
112
  def _stream_entity_request(self,
86
113
  method: str,
@@ -210,7 +237,9 @@ class DeletableEntityApi(EntityBaseApi[T]):
210
237
  httpx.HTTPStatusError: If deletion fails or any entity not found
211
238
  """
212
239
  async def _delete_all_async():
213
- async with aiohttp.ClientSession() as session:
240
+ connector = self._create_aiohttp_connector(force_close=True)
241
+ timeout = aiohttp.ClientTimeout(total=None, connect=60, sock_read=300)
242
+ async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session:
214
243
  tasks = [
215
244
  self._delete_async(entity, session)
216
245
  for entity in entities
@@ -248,7 +277,16 @@ class CreatableEntityApi(EntityBaseApi[T]):
248
277
  This class adds methods to handle creation of new entities.
249
278
  """
250
279
 
251
- def _create(self, entity_data: dict[str, Any]) -> str | list[str | dict]:
280
+ @overload
281
+ def _create(self, entity_data: dict[str, Any],
282
+ return_entity: Literal[True] = True) -> T | list[T]: ...
283
+
284
+ @overload
285
+ def _create(self, entity_data: dict[str, Any],
286
+ return_entity: Literal[False]) -> str | list: ...
287
+
288
+ def _create(self, entity_data: dict[str, Any],
289
+ return_entity: bool = False) -> str | T | list:
252
290
  """Create a new entity.
253
291
 
254
292
  Args:
@@ -263,14 +301,35 @@ class CreatableEntityApi(EntityBaseApi[T]):
263
301
  response = self._make_request('POST', f'/{self.endpoint_base}', json=entity_data)
264
302
  respdata = response.json()
265
303
  if isinstance(respdata, str):
304
+ if return_entity:
305
+ return self.get_by_id(respdata)
266
306
  return respdata
267
307
  if isinstance(respdata, list):
308
+ if return_entity:
309
+ logger.warning("Current implementation is slow when returning entities on bulk create."
310
+ " Try ``return_entity=False`` for better performance.")
311
+ return [self.get_by_id(item['id']) if isinstance(item, dict) and 'id' in item else self.get_by_id(item)
312
+ for item in respdata]
268
313
  return respdata
269
314
  if isinstance(respdata, dict):
315
+ if return_entity:
316
+ try:
317
+ return self._init_entity_obj(**respdata)
318
+ except:
319
+ logger.debug("Failed to init entity obj on create response. Falling back to get_by_id.")
320
+ return self.get_by_id(respdata.get('id'))
270
321
  return respdata.get('id')
271
322
  return respdata
272
323
 
273
- def create(self, *args, **kwargs) -> str | T:
324
+ @overload
325
+ def create(self, *args, return_entity: Literal[True] = True, **kwargs) -> T: ...
326
+
327
+ @overload
328
+ def create(self, *args, return_entity: Literal[False], **kwargs) -> str: ...
329
+
330
+ def create(self, *args,
331
+ return_entity: bool = True,
332
+ **kwargs) -> str | T:
274
333
  raise NotImplementedError("Subclasses must implement the create method with their own custom parameters")
275
334
 
276
335
 
@@ -30,7 +30,6 @@ ResourceFields: TypeAlias = Literal['modality', 'created_by', 'published_by', 'p
30
30
 
31
31
  _PAGE_LIMIT = 5000
32
32
 
33
-
34
33
  @deprecated(reason="Please use `from datamint import Api` instead.", version="2.0.0")
35
34
  class BaseAPIHandler:
36
35
  """
@@ -178,6 +178,8 @@ class CreateAnnotationDto:
178
178
  if model_id is not None:
179
179
  if is_model == False:
180
180
  raise ValueError("model_id==False while self.model_id is provided.")
181
+ if not isinstance(model_id, str):
182
+ raise ValueError("model_id must be a string if provided.")
181
183
  is_model = True
182
184
  self.is_model = is_model
183
185
  self.geometry = geometry
@@ -6,7 +6,6 @@ from rich.prompt import Prompt, Confirm
6
6
  from rich.console import Console
7
7
  import os
8
8
  import shutil
9
- from pathlib import Path
10
9
  from rich.table import Table
11
10
 
12
11
  _LOGGER = logging.getLogger(__name__)
@@ -574,6 +574,8 @@ def _parse_args() -> tuple[Any, list[str], Optional[list[dict]], Optional[list[s
574
574
  help='Automatically detect and include JSON metadata files with the same base name as NIFTI files')
575
575
  parser.add_argument('--no-auto-detect-json', dest='auto_detect_json', action='store_false',
576
576
  help='Disable automatic detection of JSON metadata files (default behavior)')
577
+ parser.add_argument('--no-assemble-dicoms', dest='assemble_dicoms', action='store_false', default=True,
578
+ help='Do not assemble DICOM files into series (default: assemble them)')
577
579
  parser.add_argument('--version', action='version', version=f'%(prog)s {datamint_version}')
578
580
  parser.add_argument('--verbose', action='store_true', help='Print debug messages', default=False)
579
581
  args = parser.parse_args()
@@ -797,7 +799,7 @@ def main():
797
799
  publish_to=args.project,
798
800
  segmentation_files=segfiles,
799
801
  transpose_segmentation=args.transpose_segmentation,
800
- assemble_dicoms=True,
802
+ assemble_dicoms=args.assemble_dicoms,
801
803
  metadata=metadata_files,
802
804
  progress_bar=True
803
805
  )
datamint/configs.py CHANGED
@@ -1,10 +1,8 @@
1
1
  import yaml
2
2
  import os
3
3
  import logging
4
- from netrc import netrc
5
4
  from platformdirs import PlatformDirs
6
- from typing import Dict
7
- from pathlib import Path
5
+ from typing import Any
8
6
 
9
7
  APIURL_KEY = 'default_api_url'
10
8
  APIKEY_KEY = 'api_key'
@@ -14,6 +12,10 @@ ENV_VARS = {
14
12
  APIURL_KEY: 'DATAMINT_API_URL'
15
13
  }
16
14
 
15
+ DEFAULT_VALUES = {
16
+ APIURL_KEY: 'https://api.datamint.io'
17
+ }
18
+
17
19
  _LOGGER = logging.getLogger(__name__)
18
20
 
19
21
  DIRS = PlatformDirs(appname='datamintapi')
@@ -23,17 +25,19 @@ try:
23
25
  except Exception as e:
24
26
  _LOGGER.error(f"Could not determine home directory: {e}")
25
27
  DATAMINT_DATA_DIR = None
26
-
27
28
 
28
29
 
29
30
  def get_env_var_name(key: str) -> str:
30
31
  return ENV_VARS[key]
31
32
 
32
- def read_config() -> Dict:
33
+
34
+ def read_config() -> dict[str, Any]:
33
35
  if os.path.exists(CONFIG_FILE):
34
36
  with open(CONFIG_FILE, 'r') as configfile:
35
- return yaml.safe_load(configfile)
36
- return {}
37
+ config = yaml.safe_load(configfile)
38
+ config.update({k: v for k, v in DEFAULT_VALUES.items() if k not in config})
39
+ return config
40
+ return DEFAULT_VALUES.copy()
37
41
 
38
42
 
39
43
  def set_value(key: str,
@@ -307,6 +307,10 @@ class DatamintBaseDataset:
307
307
  self.image_lsets, self.image_lcodes = self._get_labels_set(framed=False)
308
308
  worklist_id = self.get_info()['worklist_id']
309
309
  groups: dict[str, dict] = self.api.annotationsets.get_segmentation_group(worklist_id)['groups']
310
+ if not groups:
311
+ self.seglabel_list = []
312
+ self.seglabel2code = {}
313
+ return
310
314
  # order by 'index' key
311
315
  max_index = max([g['index'] for g in groups.values()])
312
316
  self.seglabel_list : list[str] = ['UNKNOWN'] * max_index # 1-based
@@ -928,6 +932,7 @@ class DatamintBaseDataset:
928
932
  # Collect all segmentation annotations that need to be downloaded
929
933
  segmentations_to_download = []
930
934
  segmentation_paths = []
935
+ segmentation_resource_map = {} # Maps annotation ID to resource ID for cleanup
931
936
 
932
937
  # update annotations in resources
933
938
  for resource in self.images_metainfo:
@@ -954,6 +959,7 @@ class DatamintBaseDataset:
954
959
  filepath.parent.mkdir(parents=True, exist_ok=True)
955
960
  segmentations_to_download.append(ann)
956
961
  segmentation_paths.append(filepath)
962
+ segmentation_resource_map[ann.id] = resource_id
957
963
 
958
964
  # Process annotation changes
959
965
  for ann in annotations_to_remove:
@@ -971,14 +977,28 @@ class DatamintBaseDataset:
971
977
  _LOGGER.error(f"Error deleting annotation file {filepath}: {e}")
972
978
 
973
979
  # Update resource annotations list - convert to Annotation objects
974
- # resource['annotations'] = [Annotation.from_dict(ann) for ann in new_resource_annotations]
975
980
  resource['annotations'] = new_resource_annotations
976
981
 
977
982
  # Batch download all segmentation files
978
983
  if segmentations_to_download:
979
984
  _LOGGER.info(f"Downloading {len(segmentations_to_download)} segmentation files...")
980
- self.api.annotations.download_multiple_files(segmentations_to_download, segmentation_paths)
981
- _LOGGER.info(f"Downloaded {len(segmentations_to_download)} segmentation files.")
985
+ download_results = self.api.annotations.download_multiple_files(
986
+ segmentations_to_download, segmentation_paths
987
+ )
988
+
989
+ # Process failed downloads
990
+ failed_annotations = [result['annotation_id'] for result in download_results if not result['success']]
991
+ if failed_annotations:
992
+ _LOGGER.warning(f"Failed to download {len(failed_annotations)} annotations, removing them from metadata")
993
+
994
+ # Remove failed annotations from each resource's annotation list
995
+ for resource in self.images_metainfo:
996
+ resource['annotations'] = [
997
+ ann for ann in resource['annotations']
998
+ if ann.id not in failed_annotations
999
+ ]
1000
+
1001
+ _LOGGER.info(f"Successfully downloaded {len(segmentations_to_download) - len(failed_annotations)} segmentation files.")
982
1002
 
983
1003
  ###################
984
1004
  # update metadata
@@ -1050,7 +1070,7 @@ class DatamintBaseDataset:
1050
1070
  # delete associated annotations
1051
1071
  for ann in deleted_metainfo.get('annotations', []):
1052
1072
  ann_file = getattr(ann, 'file', None) if hasattr(ann, 'file') else ann.get('file', None)
1053
- if ann_file is not None:
1073
+ if ann_file is not None and os.path.exists(os.path.join(self.dataset_dir, ann_file)):
1054
1074
  os.remove(os.path.join(self.dataset_dir, ann_file))
1055
1075
 
1056
1076
  def __add__(self, other):
@@ -7,7 +7,7 @@ import numpy as np
7
7
  import logging
8
8
  from PIL import Image
9
9
  import albumentations
10
- from datamint.entities.annotation import Annotation
10
+ from datamint.entities.annotations.annotation import Annotation
11
11
  from medimgkit.readers import read_array_normalized
12
12
 
13
13
  _LOGGER = logging.getLogger(__name__)
@@ -1,6 +1,6 @@
1
1
  """DataMint entities package."""
2
2
 
3
- from .annotation import Annotation
3
+ from .annotations.annotation import Annotation
4
4
  from .base_entity import BaseEntity
5
5
  from .channel import Channel, ChannelResourceData
6
6
  from .project import Project
@@ -0,0 +1,13 @@
1
+ from .image_classification import ImageClassification
2
+ from .image_segmentation import ImageSegmentation
3
+ from .annotation import Annotation
4
+ from .volume_segmentation import VolumeSegmentation
5
+ from datamint.api.dto import AnnotationType # FIXME: move this to this module
6
+
7
+ __all__ = [
8
+ "ImageClassification",
9
+ "ImageSegmentation",
10
+ "Annotation",
11
+ "VolumeSegmentation",
12
+ "AnnotationType",
13
+ ]
@@ -9,16 +9,17 @@ from typing import TYPE_CHECKING, Any
9
9
  import logging
10
10
  import os
11
11
 
12
- from .base_entity import BaseEntity, MISSING_FIELD
13
- from .cache_manager import CacheManager
12
+ from ..base_entity import BaseEntity, MISSING_FIELD
13
+ from ..cache_manager import CacheManager
14
14
  from pydantic import PrivateAttr
15
15
  from datetime import datetime
16
16
  from datamint.api.dto import AnnotationType
17
17
  from datamint.types import ImagingData
18
18
 
19
+
19
20
  if TYPE_CHECKING:
20
21
  from datamint.api.endpoints.annotations_api import AnnotationsApi
21
- from .resource import Resource
22
+ from ..resource import Resource
22
23
 
23
24
  logger = logging.getLogger(__name__)
24
25
 
@@ -33,7 +34,28 @@ _FIELD_MAPPING = {
33
34
  _ANNOTATION_CACHE_KEY = "annotation_data"
34
35
 
35
36
 
36
- class Annotation(BaseEntity):
37
+ class AnnotationBase(BaseEntity):
38
+ """Minimal base class for creating annotations.
39
+
40
+ This class contains only the essential fields needed to create annotations.
41
+ Use this for creating specific annotation types like ImageClassification.
42
+ """
43
+
44
+ identifier: str
45
+ scope: str
46
+ annotation_type: AnnotationType
47
+ confiability: float = 1.0
48
+
49
+ def __init__(self, **data):
50
+ """Initialize the annotation base entity."""
51
+ super().__init__(**data)
52
+
53
+ @property
54
+ def name(self) -> str:
55
+ """Get the annotation name (alias for identifier)."""
56
+ return self.identifier
57
+
58
+ class Annotation(AnnotationBase):
37
59
  """Pydantic Model representing a DataMint annotation.
38
60
 
39
61
  Attributes:
@@ -67,32 +89,31 @@ class Annotation(BaseEntity):
67
89
  values: Optional extra values payload for flexible schemas.
68
90
  """
69
91
 
70
- id: str
92
+ id: str | None = None
71
93
  identifier: str
72
94
  scope: str
73
- frame_index: int | None
74
- annotation_type: AnnotationType
75
- text_value: str | None
76
- numeric_value: float | int | None
77
- units: str | None
78
- geometry: list | dict | None
79
- created_at: str # ISO timestamp string
80
- created_by: str
81
- annotation_worklist_id: str | None
82
- status: str
83
- approved_at: str | None # ISO timestamp string
84
- approved_by: str | None
85
- resource_id: str
86
- associated_file: str | None
87
- deleted: bool
88
- deleted_at: str | None # ISO timestamp string
89
- deleted_by: str | None
90
- created_by_model: str | None
91
- set_name: str | None
92
- resource_filename: str | None
93
- resource_modality: str | None
94
- annotation_worklist_name: str | None
95
- user_info: dict | None
95
+ frame_index: int | None = None
96
+ text_value: str | None = None
97
+ numeric_value: float | int | None = None
98
+ units: str | None = None
99
+ geometry: list | dict | None = None
100
+ created_at: str | None = None # ISO timestamp string
101
+ created_by: str | None = None
102
+ annotation_worklist_id: str | None = None
103
+ status: str | None = None
104
+ approved_at: str | None = None # ISO timestamp string
105
+ approved_by: str | None = None
106
+ resource_id: str | None = None
107
+ associated_file: str | None = None
108
+ deleted: bool = False
109
+ deleted_at: str | None = None # ISO timestamp string
110
+ deleted_by: str | None = None
111
+ created_by_model: str | None = None
112
+ set_name: str | None = None
113
+ resource_filename: str | None = None
114
+ resource_modality: str | None = None
115
+ annotation_worklist_name: str | None = None
116
+ user_info: dict | None = None
96
117
  values: list | None = MISSING_FIELD
97
118
  file: str | None = None
98
119
 
@@ -101,9 +122,14 @@ class Annotation(BaseEntity):
101
122
  def __init__(self, **data):
102
123
  """Initialize the annotation entity."""
103
124
  super().__init__(**data)
104
- self._cache: CacheManager = CacheManager('annotations')
105
125
  self._resource: 'Resource | None' = None
106
126
 
127
+ @property
128
+ def _cache(self) -> CacheManager[bytes]:
129
+ if not hasattr(self, '__cache'):
130
+ self.__cache = CacheManager[bytes]('annotations')
131
+ return self.__cache
132
+
107
133
  @property
108
134
  def resource(self) -> 'Resource':
109
135
  """Lazily load and cache the associated Resource entity."""
@@ -117,24 +143,37 @@ class Annotation(BaseEntity):
117
143
  auto_convert: bool = True,
118
144
  use_cache: bool = False,
119
145
  ) -> bytes | ImagingData:
146
+ """Get the file data for this annotation.
147
+
148
+ Args:
149
+ save_path: Optional path to save the file locally. If use_cache is also True,
150
+ the file is saved to save_path and cache metadata points to that location
151
+ (no duplication - only one file on disk).
152
+ auto_convert: If True, automatically converts to appropriate format
153
+ use_cache: If True, uses cached data when available and valid
154
+
155
+ Returns:
156
+ File data (format depends on auto_convert and file type)
157
+ """
120
158
  # Version info for cache validation
121
159
  version_info = self._generate_version_info()
122
160
 
123
- # Try to get from cache
124
- img_data = None
125
- if use_cache:
126
- img_data = self._cache.get(self.id, _ANNOTATION_CACHE_KEY, version_info)
127
-
128
- if img_data is None:
129
- # Fetch from server using download_resource_file
130
- logger.debug(f"Fetching image data from server for resource {self.id}")
131
- img_data = self._api.download_file(
161
+ # Download callback for the shared caching logic
162
+ def download_callback(path: str | None) -> bytes:
163
+ return self._api.download_file(
132
164
  self,
133
- fpath_out=save_path
165
+ fpath_out=path
134
166
  )
135
- # Cache the data
136
- if use_cache:
137
- self._cache.set(self.id, _ANNOTATION_CACHE_KEY, img_data, version_info)
167
+
168
+ # Use shared caching logic from BaseEntity
169
+ img_data = self._fetch_and_cache_file_data(
170
+ cache_manager=self._cache,
171
+ data_key=_ANNOTATION_CACHE_KEY,
172
+ version_info=version_info,
173
+ download_callback=download_callback,
174
+ save_path=save_path,
175
+ use_cache=use_cache,
176
+ )
138
177
 
139
178
  if auto_convert:
140
179
  return self._api.convert_format(img_data)
@@ -190,11 +229,6 @@ class Annotation(BaseEntity):
190
229
  """Alias for :attr:`annotation_type`."""
191
230
  return self.annotation_type
192
231
 
193
- @property
194
- def name(self) -> str:
195
- """Get the annotation name (alias for identifier)."""
196
- return self.identifier
197
-
198
232
  @property
199
233
  def index(self) -> int | None:
200
234
  """Get the frame index (alias for frame_index)."""
@@ -0,0 +1,12 @@
1
+ from .annotation import Annotation
2
+ from datamint.api.dto import AnnotationType
3
+
4
+
5
+ class ImageClassification(Annotation):
6
+ def __init__(self,
7
+ name: str,
8
+ value: str,
9
+ confiability: float = 1.0):
10
+ super().__init__(identifier=name, text_value=value, scope='image',
11
+ confiability=confiability,
12
+ annotation_type=AnnotationType.CATEGORY)