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.
- datamint/__init__.py +1 -3
- datamint/api/__init__.py +0 -3
- datamint/api/base_api.py +286 -54
- datamint/api/client.py +76 -13
- datamint/api/endpoints/__init__.py +2 -2
- datamint/api/endpoints/annotations_api.py +186 -28
- datamint/api/endpoints/deploy_model_api.py +78 -0
- datamint/api/endpoints/models_api.py +1 -0
- datamint/api/endpoints/projects_api.py +38 -7
- datamint/api/endpoints/resources_api.py +227 -100
- datamint/api/entity_base_api.py +66 -7
- datamint/apihandler/base_api_handler.py +0 -1
- datamint/apihandler/dto/annotation_dto.py +2 -0
- datamint/client_cmd_tools/datamint_config.py +0 -1
- datamint/client_cmd_tools/datamint_upload.py +3 -1
- datamint/configs.py +11 -7
- datamint/dataset/base_dataset.py +24 -4
- datamint/dataset/dataset.py +1 -1
- datamint/entities/__init__.py +1 -1
- datamint/entities/annotations/__init__.py +13 -0
- datamint/entities/{annotation.py → annotations/annotation.py} +81 -47
- datamint/entities/annotations/image_classification.py +12 -0
- datamint/entities/annotations/image_segmentation.py +252 -0
- datamint/entities/annotations/volume_segmentation.py +273 -0
- datamint/entities/base_entity.py +100 -6
- datamint/entities/cache_manager.py +129 -15
- datamint/entities/datasetinfo.py +60 -65
- datamint/entities/deployjob.py +18 -0
- datamint/entities/project.py +39 -0
- datamint/entities/resource.py +310 -46
- datamint/lightning/__init__.py +1 -0
- datamint/lightning/datamintdatamodule.py +103 -0
- datamint/mlflow/__init__.py +65 -0
- datamint/mlflow/artifact/__init__.py +1 -0
- datamint/mlflow/artifact/datamint_artifacts_repo.py +8 -0
- datamint/mlflow/env_utils.py +131 -0
- datamint/mlflow/env_vars.py +5 -0
- datamint/mlflow/flavors/__init__.py +17 -0
- datamint/mlflow/flavors/datamint_flavor.py +150 -0
- datamint/mlflow/flavors/model.py +877 -0
- datamint/mlflow/lightning/callbacks/__init__.py +1 -0
- datamint/mlflow/lightning/callbacks/modelcheckpoint.py +410 -0
- datamint/mlflow/models/__init__.py +93 -0
- datamint/mlflow/tracking/datamint_store.py +76 -0
- datamint/mlflow/tracking/default_experiment.py +27 -0
- datamint/mlflow/tracking/fluent.py +91 -0
- datamint/utils/env.py +27 -0
- datamint/utils/visualization.py +21 -13
- datamint-2.9.0.dist-info/METADATA +220 -0
- datamint-2.9.0.dist-info/RECORD +73 -0
- {datamint-2.3.3.dist-info → datamint-2.9.0.dist-info}/WHEEL +1 -1
- datamint-2.9.0.dist-info/entry_points.txt +18 -0
- datamint/apihandler/exp_api_handler.py +0 -204
- datamint/experiment/__init__.py +0 -1
- datamint/experiment/_patcher.py +0 -570
- datamint/experiment/experiment.py +0 -1049
- datamint-2.3.3.dist-info/METADATA +0 -125
- datamint-2.3.3.dist-info/RECORD +0 -54
- datamint-2.3.3.dist-info/entry_points.txt +0 -4
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
from typing import
|
|
1
|
+
from typing import Literal, BinaryIO, IO, Any, overload
|
|
2
|
+
from collections.abc import Sequence, Generator
|
|
2
3
|
import httpx
|
|
3
4
|
from datetime import date
|
|
4
5
|
import logging
|
|
5
6
|
from ..entity_base_api import ApiConfig, CreatableEntityApi, DeletableEntityApi
|
|
6
|
-
from .
|
|
7
|
-
from datamint.entities.annotation import Annotation
|
|
7
|
+
from datamint.entities.annotations.annotation import Annotation
|
|
8
8
|
from datamint.entities.resource import Resource
|
|
9
|
-
from datamint.entities.project import Project
|
|
10
9
|
from datamint.api.dto import AnnotationType, CreateAnnotationDto, LineGeometry, BoxGeometry, CoordinateSystem, Geometry
|
|
11
10
|
import numpy as np
|
|
12
11
|
import os
|
|
@@ -43,12 +42,16 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
|
|
|
43
42
|
client: Optional HTTP client instance. If None, a new one will be created.
|
|
44
43
|
"""
|
|
45
44
|
from .resources_api import ResourcesApi
|
|
45
|
+
from .models_api import ModelsApi
|
|
46
|
+
|
|
46
47
|
super().__init__(config, Annotation, 'annotations', client)
|
|
47
48
|
self._models_api = ModelsApi(config, client=client) if models_api is None else models_api
|
|
48
|
-
self._resources_api = ResourcesApi(
|
|
49
|
+
self._resources_api = ResourcesApi(
|
|
50
|
+
config, client=client, annotations_api=self) if resources_api is None else resources_api
|
|
49
51
|
|
|
52
|
+
@overload
|
|
50
53
|
def get_list(self,
|
|
51
|
-
resource: str | Resource | None = None,
|
|
54
|
+
resource: str | Resource | Sequence[str | Resource] | None = None,
|
|
52
55
|
annotation_type: AnnotationType | str | None = None,
|
|
53
56
|
annotator_email: str | None = None,
|
|
54
57
|
date_from: date | None = None,
|
|
@@ -57,10 +60,88 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
|
|
|
57
60
|
worklist_id: str | None = None,
|
|
58
61
|
status: Literal['new', 'published'] | None = None,
|
|
59
62
|
load_ai_segmentations: bool | None = None,
|
|
60
|
-
limit: int | None = None
|
|
61
|
-
|
|
63
|
+
limit: int | None = None,
|
|
64
|
+
group_by_resource: Literal[False] = False
|
|
65
|
+
) -> Sequence[Annotation]: ...
|
|
66
|
+
|
|
67
|
+
@overload
|
|
68
|
+
def get_list(self,
|
|
69
|
+
resource: str | Resource | Sequence[str | Resource] | None = None,
|
|
70
|
+
annotation_type: AnnotationType | str | None = None,
|
|
71
|
+
annotator_email: str | None = None,
|
|
72
|
+
date_from: date | None = None,
|
|
73
|
+
date_to: date | None = None,
|
|
74
|
+
dataset_id: str | None = None,
|
|
75
|
+
worklist_id: str | None = None,
|
|
76
|
+
status: Literal['new', 'published'] | None = None,
|
|
77
|
+
load_ai_segmentations: bool | None = None,
|
|
78
|
+
limit: int | None = None,
|
|
79
|
+
*,
|
|
80
|
+
group_by_resource: Literal[True]
|
|
81
|
+
) -> Sequence[Sequence[Annotation]]: ...
|
|
82
|
+
|
|
83
|
+
def get_list(self,
|
|
84
|
+
resource: str | Resource | Sequence[str | Resource] | None = None,
|
|
85
|
+
annotation_type: AnnotationType | str | None = None,
|
|
86
|
+
annotator_email: str | None = None,
|
|
87
|
+
date_from: date | None = None,
|
|
88
|
+
date_to: date | None = None,
|
|
89
|
+
dataset_id: str | None = None,
|
|
90
|
+
worklist_id: str | None = None,
|
|
91
|
+
status: Literal['new', 'published'] | None = None,
|
|
92
|
+
load_ai_segmentations: bool | None = None,
|
|
93
|
+
limit: int | None = None,
|
|
94
|
+
group_by_resource: bool = False
|
|
95
|
+
) -> Sequence[Annotation] | Sequence[Sequence[Annotation]]:
|
|
96
|
+
"""
|
|
97
|
+
Retrieve a list of annotations with optional filtering.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
resource: The resource unique id(s) or Resource instance(s). Can be a single resource,
|
|
101
|
+
a list of resources, or None to retrieve annotations from all resources.
|
|
102
|
+
annotation_type: Filter by annotation type (e.g., 'segmentation', 'category').
|
|
103
|
+
annotator_email: Filter by annotator email address.
|
|
104
|
+
date_from: Filter annotations created on or after this date.
|
|
105
|
+
date_to: Filter annotations created on or before this date.
|
|
106
|
+
dataset_id: Filter by dataset unique id.
|
|
107
|
+
worklist_id: Filter by annotation worklist unique id.
|
|
108
|
+
status: Filter by annotation status ('new' or 'published').
|
|
109
|
+
load_ai_segmentations: Whether to load AI-generated segmentations.
|
|
110
|
+
limit: Maximum number of annotations to return.
|
|
111
|
+
group_by_resource: If True, return results grouped by resource.
|
|
112
|
+
For instance, the first index of the returned list will contain all annotations for the first resource.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Sequence[Annotation] | Sequence[Sequence[Annotation]]: List of annotations, or list of lists if grouped by resource.
|
|
116
|
+
|
|
117
|
+
Example:
|
|
118
|
+
.. code-block:: python
|
|
119
|
+
|
|
120
|
+
# Get all annotations for a single resource
|
|
121
|
+
annotations = api.annotations.get_list(resource='resource_id')
|
|
122
|
+
|
|
123
|
+
# Get annotations with filters
|
|
124
|
+
annotations = api.annotations.get_list(
|
|
125
|
+
resource='resource_id',
|
|
126
|
+
annotation_type='segmentation',
|
|
127
|
+
status='published'
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Get annotations for multiple resources
|
|
131
|
+
annotations = api.annotations.get_list(
|
|
132
|
+
resource=['resource_id_1', 'resource_id_2', 'resource_id_3']
|
|
133
|
+
)
|
|
134
|
+
"""
|
|
135
|
+
def group_annotations_by_resource(annotations: Sequence[Annotation],
|
|
136
|
+
resource_ids: Sequence[str]
|
|
137
|
+
) -> Sequence[Sequence[Annotation]]:
|
|
138
|
+
resource_annotations_map = {rid: [] for rid in resource_ids}
|
|
139
|
+
for ann in annotations:
|
|
140
|
+
resource_annotations_map[ann.resource_id].append(ann)
|
|
141
|
+
return [resource_annotations_map[rid] for rid in resource_ids]
|
|
142
|
+
|
|
143
|
+
# Build search payload according to POST /annotations/search schema
|
|
62
144
|
payload = {
|
|
63
|
-
'resource_id': resource.id if isinstance(resource, Resource) else resource,
|
|
64
145
|
'annotation_type': annotation_type,
|
|
65
146
|
'annotatorEmail': annotator_email,
|
|
66
147
|
'from': date_from.isoformat() if date_from is not None else None,
|
|
@@ -68,12 +149,37 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
|
|
|
68
149
|
'dataset_id': dataset_id,
|
|
69
150
|
'annotation_worklist_id': worklist_id,
|
|
70
151
|
'status': status,
|
|
71
|
-
'load_ai_segmentations': load_ai_segmentations
|
|
152
|
+
'load_ai_segmentations': load_ai_segmentations,
|
|
72
153
|
}
|
|
73
154
|
|
|
74
|
-
|
|
155
|
+
if isinstance(resource, (str, Resource)):
|
|
156
|
+
resource_id = self._entid(resource)
|
|
157
|
+
payload['resource_id'] = resource_id
|
|
158
|
+
resource_ids = None
|
|
159
|
+
elif resource is not None:
|
|
160
|
+
resource_ids = [self._entid(res) for res in resource]
|
|
161
|
+
payload['resource_ids'] = resource_ids
|
|
162
|
+
else:
|
|
163
|
+
resource_ids = None
|
|
164
|
+
|
|
165
|
+
# Remove None values from payload
|
|
75
166
|
payload = {k: v for k, v in payload.items() if v is not None}
|
|
76
|
-
|
|
167
|
+
|
|
168
|
+
items_gen = self._make_request_with_pagination('POST',
|
|
169
|
+
f'{self.endpoint_base}/search',
|
|
170
|
+
return_field=self.endpoint_base,
|
|
171
|
+
limit=limit,
|
|
172
|
+
json=payload)
|
|
173
|
+
|
|
174
|
+
all_items = []
|
|
175
|
+
for _, items in items_gen:
|
|
176
|
+
all_items.extend(items)
|
|
177
|
+
|
|
178
|
+
all_annotations = [self._init_entity_obj(**item) for item in all_items]
|
|
179
|
+
|
|
180
|
+
if group_by_resource and resource_ids is not None:
|
|
181
|
+
return group_annotations_by_resource(all_annotations, resource_ids)
|
|
182
|
+
return all_annotations
|
|
77
183
|
|
|
78
184
|
async def _upload_segmentations_async(self,
|
|
79
185
|
resource: str | Resource,
|
|
@@ -371,14 +477,21 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
|
|
|
371
477
|
resource: str | Resource,
|
|
372
478
|
annotation_dto: CreateAnnotationDto | Sequence[CreateAnnotationDto]
|
|
373
479
|
) -> str | Sequence[str]:
|
|
374
|
-
"""Create a
|
|
480
|
+
"""Create one or more annotations for a resource.
|
|
481
|
+
|
|
482
|
+
.. warning::
|
|
483
|
+
This is an internal method and should not be used directly by users.
|
|
484
|
+
Please use specific annotation creation methods like
|
|
485
|
+
:py:meth:`create_image_classification` or :py:meth:`upload_segmentations` instead.
|
|
375
486
|
|
|
376
487
|
Args:
|
|
377
|
-
resource: The resource unique id or Resource instance.
|
|
378
|
-
annotation_dto
|
|
488
|
+
resource (str | Resource): The resource unique id or Resource instance.
|
|
489
|
+
annotation_dto (CreateAnnotationDto | Sequence[CreateAnnotationDto]):
|
|
490
|
+
A CreateAnnotationDto instance or a list of such instances to be created.
|
|
379
491
|
|
|
380
492
|
Returns:
|
|
381
|
-
The id of the created annotation
|
|
493
|
+
str | Sequence[str]: The id of the created annotation if a single annotation
|
|
494
|
+
was provided, or a list of ids if multiple annotations were created.
|
|
382
495
|
"""
|
|
383
496
|
|
|
384
497
|
annotations = [annotation_dto] if isinstance(annotation_dto, CreateAnnotationDto) else annotation_dto
|
|
@@ -396,7 +509,7 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
|
|
|
396
509
|
|
|
397
510
|
def upload_segmentations(self,
|
|
398
511
|
resource: str | Resource,
|
|
399
|
-
file_path: str | np.ndarray,
|
|
512
|
+
file_path: str | Path | np.ndarray,
|
|
400
513
|
name: str | dict[int, str] | dict[tuple, str] | None = None,
|
|
401
514
|
frame_index: int | list[int] | None = None,
|
|
402
515
|
imported_from: str | None = None,
|
|
@@ -460,6 +573,9 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
|
|
|
460
573
|
"""
|
|
461
574
|
import nest_asyncio
|
|
462
575
|
|
|
576
|
+
if isinstance(file_path, Path):
|
|
577
|
+
file_path = str(file_path)
|
|
578
|
+
|
|
463
579
|
if isinstance(file_path, str) and not os.path.exists(file_path):
|
|
464
580
|
raise FileNotFoundError(f"File {file_path} not found.")
|
|
465
581
|
|
|
@@ -661,6 +777,7 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
|
|
|
661
777
|
if isinstance(file_path, str):
|
|
662
778
|
if file_path.endswith('.nii') or file_path.endswith('.nii.gz'):
|
|
663
779
|
# Upload NIfTI file directly
|
|
780
|
+
_LOGGER.debug('uploading segmentation as a volume')
|
|
664
781
|
with open(file_path, 'rb') as f:
|
|
665
782
|
filename = os.path.basename(file_path)
|
|
666
783
|
form = aiohttp.FormData()
|
|
@@ -672,9 +789,17 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
|
|
|
672
789
|
if name is not None:
|
|
673
790
|
form.add_field('segmentation_map', json.dumps(name), content_type='application/json')
|
|
674
791
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
792
|
+
try:
|
|
793
|
+
respdata = await self._make_request_async_json(
|
|
794
|
+
'POST',
|
|
795
|
+
f'{self.endpoint_base}/{resource_id}/segmentations/file',
|
|
796
|
+
data=form
|
|
797
|
+
)
|
|
798
|
+
except ResourceNotFoundError as e:
|
|
799
|
+
e.resource_type = 'resource'
|
|
800
|
+
e.params = {'resource_id': resource_id}
|
|
801
|
+
raise e
|
|
802
|
+
|
|
678
803
|
if 'error' in respdata:
|
|
679
804
|
raise DatamintException(respdata['error'])
|
|
680
805
|
return respdata
|
|
@@ -815,7 +940,7 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
|
|
|
815
940
|
model_id=model_id
|
|
816
941
|
)
|
|
817
942
|
|
|
818
|
-
return self.create(resource, annotation_dto)
|
|
943
|
+
return self.create(resource, annotation_dto)
|
|
819
944
|
|
|
820
945
|
def add_line_annotation(self,
|
|
821
946
|
point1: tuple[int, int] | tuple[float, float, float],
|
|
@@ -977,13 +1102,23 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
|
|
|
977
1102
|
save_path (str | Path): The path to save the file.
|
|
978
1103
|
session (aiohttp.ClientSession): The aiohttp session to use for the request.
|
|
979
1104
|
progress_bar (tqdm | None): Optional progress bar to update after download completion.
|
|
1105
|
+
|
|
1106
|
+
Returns:
|
|
1107
|
+
dict: A dictionary with 'success' (bool) and optional 'error' (str) keys.
|
|
980
1108
|
"""
|
|
981
1109
|
if isinstance(annotation, Annotation):
|
|
982
1110
|
annotation_id = annotation.id
|
|
983
1111
|
resource_id = annotation.resource_id
|
|
984
1112
|
else:
|
|
985
1113
|
annotation_id = annotation
|
|
986
|
-
|
|
1114
|
+
try:
|
|
1115
|
+
resource_id = self.get_by_id(annotation_id).resource_id
|
|
1116
|
+
except Exception as e:
|
|
1117
|
+
error_msg = f"Failed to get resource_id for annotation {annotation_id}: {str(e)}"
|
|
1118
|
+
_LOGGER.error(error_msg)
|
|
1119
|
+
if progress_bar:
|
|
1120
|
+
progress_bar.update(1)
|
|
1121
|
+
return {'success': False, 'annotation_id': annotation_id, 'error': error_msg}
|
|
987
1122
|
|
|
988
1123
|
try:
|
|
989
1124
|
async with self._make_request_async('GET',
|
|
@@ -994,20 +1129,31 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
|
|
|
994
1129
|
f.write(data_bytes)
|
|
995
1130
|
if progress_bar:
|
|
996
1131
|
progress_bar.update(1)
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1132
|
+
return {'success': True, 'annotation_id': annotation_id}
|
|
1133
|
+
except Exception as e:
|
|
1134
|
+
error_msg = f"Failed to download annotation {annotation_id}: {str(e)}"
|
|
1135
|
+
_LOGGER.error(error_msg)
|
|
1136
|
+
if progress_bar:
|
|
1137
|
+
progress_bar.update(1)
|
|
1138
|
+
return {'success': False, 'annotation_id': annotation_id, 'error': error_msg}
|
|
1000
1139
|
|
|
1001
1140
|
def download_multiple_files(self,
|
|
1002
1141
|
annotations: Sequence[str | Annotation],
|
|
1003
1142
|
save_paths: Sequence[str | Path] | str
|
|
1004
|
-
) ->
|
|
1143
|
+
) -> list[dict[str, Any]]:
|
|
1005
1144
|
"""
|
|
1006
1145
|
Download multiple segmentation files and save them to the specified paths.
|
|
1007
1146
|
|
|
1008
1147
|
Args:
|
|
1009
1148
|
annotations: A list of annotation unique ids or annotation objects.
|
|
1010
1149
|
save_paths: A list of paths to save the files or a directory path.
|
|
1150
|
+
|
|
1151
|
+
Returns:
|
|
1152
|
+
List of dictionaries with 'success', 'annotation_id', and optional 'error' keys.
|
|
1153
|
+
|
|
1154
|
+
Note:
|
|
1155
|
+
If any downloads fail, they will be logged but the process will continue.
|
|
1156
|
+
A summary of failed downloads will be logged at the end.
|
|
1011
1157
|
"""
|
|
1012
1158
|
import nest_asyncio
|
|
1013
1159
|
nest_asyncio.apply()
|
|
@@ -1019,7 +1165,7 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
|
|
|
1019
1165
|
annotation, save_path=path, session=session, progress_bar=progress_bar)
|
|
1020
1166
|
for annotation, path in zip(annotations, save_paths)
|
|
1021
1167
|
]
|
|
1022
|
-
await asyncio.gather(*tasks)
|
|
1168
|
+
return await asyncio.gather(*tasks)
|
|
1023
1169
|
|
|
1024
1170
|
if isinstance(save_paths, str):
|
|
1025
1171
|
save_paths = [os.path.join(save_paths, self._entid(ann))
|
|
@@ -1027,7 +1173,19 @@ class AnnotationsApi(CreatableEntityApi[Annotation], DeletableEntityApi[Annotati
|
|
|
1027
1173
|
|
|
1028
1174
|
with tqdm(total=len(annotations), desc="Downloading segmentations", unit="file") as progress_bar:
|
|
1029
1175
|
loop = asyncio.get_event_loop()
|
|
1030
|
-
loop.run_until_complete(_download_all_async())
|
|
1176
|
+
results = loop.run_until_complete(_download_all_async())
|
|
1177
|
+
|
|
1178
|
+
# Log summary of failures
|
|
1179
|
+
failures = [r for r in results if not r['success']]
|
|
1180
|
+
if failures:
|
|
1181
|
+
_LOGGER.warning(f"Failed to download {len(failures)} out of {len(annotations)} annotations")
|
|
1182
|
+
_USER_LOGGER.warning(f"Failed to download {len(failures)} out of {len(annotations)} annotations")
|
|
1183
|
+
for failure in failures:
|
|
1184
|
+
_LOGGER.debug(f" - {failure['annotation_id']}: {failure['error']}")
|
|
1185
|
+
else:
|
|
1186
|
+
_USER_LOGGER.info(f"Successfully downloaded all {len(annotations)} annotations")
|
|
1187
|
+
|
|
1188
|
+
return results
|
|
1031
1189
|
|
|
1032
1190
|
def bulk_download_file(self,
|
|
1033
1191
|
annotations: Sequence[str | Annotation],
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
from ..entity_base_api import EntityBaseApi, ApiConfig
|
|
3
|
+
from datamint.entities.deployjob import DeployJob
|
|
4
|
+
|
|
5
|
+
class DeployModelApi(EntityBaseApi[DeployJob]):
|
|
6
|
+
"""API handler for model deployment endpoints."""
|
|
7
|
+
|
|
8
|
+
def __init__(self,
|
|
9
|
+
config: ApiConfig,
|
|
10
|
+
client: httpx.Client | None = None) -> None:
|
|
11
|
+
super().__init__(config, DeployJob, 'datamint/api/v1/deploy-model', client)
|
|
12
|
+
|
|
13
|
+
def get_by_id(self, entity_id: str) -> DeployJob:
|
|
14
|
+
"""Get deployment job status by ID."""
|
|
15
|
+
response = self._make_request('GET', f'/{self.endpoint_base}/status/{entity_id}')
|
|
16
|
+
data = response.json()
|
|
17
|
+
if 'job_id' in data:
|
|
18
|
+
data['id'] = data.pop('job_id')
|
|
19
|
+
return self._init_entity_obj(**data)
|
|
20
|
+
|
|
21
|
+
def start(self,
|
|
22
|
+
model_name: str,
|
|
23
|
+
model_version: int | None = None,
|
|
24
|
+
model_alias: str | None = None,
|
|
25
|
+
image_name: str | None = None,
|
|
26
|
+
with_gpu: bool = False,
|
|
27
|
+
convert_to_onnx: bool = False,
|
|
28
|
+
input_shape: list[int] | None = None) -> DeployJob:
|
|
29
|
+
"""Start a new deployment job."""
|
|
30
|
+
payload = {
|
|
31
|
+
"model_name": model_name,
|
|
32
|
+
"model_version": model_version,
|
|
33
|
+
"model_alias": model_alias,
|
|
34
|
+
"image_name": image_name,
|
|
35
|
+
"with_gpu": with_gpu,
|
|
36
|
+
"convert_to_onnx": convert_to_onnx,
|
|
37
|
+
"input_shape": input_shape
|
|
38
|
+
}
|
|
39
|
+
# Remove None values
|
|
40
|
+
payload = {k: v for k, v in payload.items() if v is not None}
|
|
41
|
+
|
|
42
|
+
response = self._make_request('POST', f'/{self.endpoint_base}/start', json=payload)
|
|
43
|
+
data = response.json()
|
|
44
|
+
return self.get_by_id(data['job_id'])
|
|
45
|
+
|
|
46
|
+
def cancel(self, job: str | DeployJob) -> bool:
|
|
47
|
+
"""Cancel a deployment job."""
|
|
48
|
+
job_id = self._entid(job)
|
|
49
|
+
response = self._make_request('POST', f'/{self.endpoint_base}/cancel/{job_id}')
|
|
50
|
+
return response.json().get('success', False)
|
|
51
|
+
|
|
52
|
+
def list_active_jobs(self) -> dict:
|
|
53
|
+
"""List active deployment jobs count."""
|
|
54
|
+
response = self._make_request('GET', f'/{self.endpoint_base}/jobs')
|
|
55
|
+
return response.json()
|
|
56
|
+
|
|
57
|
+
def list_images(self, model_name: str | None = None) -> list[dict]:
|
|
58
|
+
"""List deployed model images."""
|
|
59
|
+
params = {}
|
|
60
|
+
if model_name:
|
|
61
|
+
params['model_name'] = model_name
|
|
62
|
+
response = self._make_request('GET', f'/{self.endpoint_base}/images', params=params)
|
|
63
|
+
return response.json()
|
|
64
|
+
|
|
65
|
+
def remove_image(self, model_name: str, tag: str | None = None) -> dict:
|
|
66
|
+
"""Remove a deployed model image."""
|
|
67
|
+
params = {}
|
|
68
|
+
if tag:
|
|
69
|
+
params['tag'] = tag
|
|
70
|
+
response = self._make_request('DELETE', f'/{self.endpoint_base}/image/{model_name}', params=params)
|
|
71
|
+
return response.json()
|
|
72
|
+
|
|
73
|
+
def image_exists(self, model_name: str, tag: str = "champion") -> bool:
|
|
74
|
+
"""Check if a model image exists."""
|
|
75
|
+
params = {'tag': tag}
|
|
76
|
+
response = self._make_request('GET', f'/{self.endpoint_base}/image/{model_name}/exists', params=params)
|
|
77
|
+
return response.json().get('exists', False)
|
|
78
|
+
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
from typing import Sequence, Literal
|
|
1
|
+
from typing import Sequence, Literal, TYPE_CHECKING, overload
|
|
2
2
|
from ..entity_base_api import ApiConfig, CRUDEntityApi
|
|
3
3
|
from datamint.entities.project import Project
|
|
4
|
-
from datamint.entities.resource import Resource
|
|
5
4
|
import httpx
|
|
5
|
+
from datamint.entities.resource import Resource
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from .resources_api import ResourcesApi
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
class ProjectsApi(CRUDEntityApi[Project]):
|
|
@@ -10,14 +12,17 @@ class ProjectsApi(CRUDEntityApi[Project]):
|
|
|
10
12
|
|
|
11
13
|
def __init__(self,
|
|
12
14
|
config: ApiConfig,
|
|
13
|
-
client: httpx.Client | None = None
|
|
15
|
+
client: httpx.Client | None = None,
|
|
16
|
+
resources_api: 'ResourcesApi | None' = None) -> None:
|
|
14
17
|
"""Initialize the projects API handler.
|
|
15
18
|
|
|
16
19
|
Args:
|
|
17
20
|
config: API configuration containing base URL, API key, etc.
|
|
18
21
|
client: Optional HTTP client instance. If None, a new one will be created.
|
|
19
22
|
"""
|
|
23
|
+
from .resources_api import ResourcesApi
|
|
20
24
|
super().__init__(config, Project, 'projects', client)
|
|
25
|
+
self.resources_api = resources_api or ResourcesApi(config, client, projects_api=self)
|
|
21
26
|
|
|
22
27
|
def get_project_resources(self, project: Project | str) -> list[Resource]:
|
|
23
28
|
"""Get resources associated with a specific project.
|
|
@@ -30,16 +35,41 @@ class ProjectsApi(CRUDEntityApi[Project]):
|
|
|
30
35
|
"""
|
|
31
36
|
response = self._get_child_entities(project, 'resources')
|
|
32
37
|
resources_data = response.json()
|
|
33
|
-
resources = [
|
|
38
|
+
resources = [self.resources_api._init_entity_obj(**item) for item in resources_data]
|
|
34
39
|
return resources
|
|
35
40
|
|
|
41
|
+
|
|
42
|
+
@overload
|
|
43
|
+
def create(self,
|
|
44
|
+
name: str,
|
|
45
|
+
description: str,
|
|
46
|
+
resources_ids: list[str] | None = None,
|
|
47
|
+
is_active_learning: bool = False,
|
|
48
|
+
two_up_display: bool = False,
|
|
49
|
+
*,
|
|
50
|
+
return_entity: Literal[True] = True
|
|
51
|
+
) -> Project: ...
|
|
52
|
+
|
|
53
|
+
@overload
|
|
54
|
+
def create(self,
|
|
55
|
+
name: str,
|
|
56
|
+
description: str,
|
|
57
|
+
resources_ids: list[str] | None = None,
|
|
58
|
+
is_active_learning: bool = False,
|
|
59
|
+
two_up_display: bool = False,
|
|
60
|
+
*,
|
|
61
|
+
return_entity: Literal[False]
|
|
62
|
+
) -> str: ...
|
|
63
|
+
|
|
36
64
|
def create(self,
|
|
37
65
|
name: str,
|
|
38
66
|
description: str,
|
|
39
67
|
resources_ids: list[str] | None = None,
|
|
40
68
|
is_active_learning: bool = False,
|
|
41
|
-
two_up_display: bool = False
|
|
42
|
-
|
|
69
|
+
two_up_display: bool = False,
|
|
70
|
+
*,
|
|
71
|
+
return_entity: bool = True
|
|
72
|
+
) -> str | Project:
|
|
43
73
|
"""Create a new project.
|
|
44
74
|
|
|
45
75
|
Args:
|
|
@@ -48,6 +78,7 @@ class ProjectsApi(CRUDEntityApi[Project]):
|
|
|
48
78
|
resources_ids: The list of resource ids to be included in the project.
|
|
49
79
|
is_active_learning: Whether the project is an active learning project or not.
|
|
50
80
|
two_up_display: Allow annotators to display multiple resources for annotation.
|
|
81
|
+
return_entity: Whether to return the created Project instance or just its ID.
|
|
51
82
|
|
|
52
83
|
Returns:
|
|
53
84
|
The id of the created project.
|
|
@@ -67,7 +98,7 @@ class ProjectsApi(CRUDEntityApi[Project]):
|
|
|
67
98
|
"require_review": False,
|
|
68
99
|
'description': description}
|
|
69
100
|
|
|
70
|
-
return self._create(project_data)
|
|
101
|
+
return self._create(project_data, return_entity=return_entity)
|
|
71
102
|
|
|
72
103
|
def get_all(self, limit: int | None = None) -> Sequence[Project]:
|
|
73
104
|
"""Get all projects.
|