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,12 +1,11 @@
1
- from typing import Any, Sequence, Literal, BinaryIO, Generator, IO
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 .models_api import ModelsApi
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(config, client=client, annotations_api=self) if resources_api is None else resources_api
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
- ) -> Sequence[Annotation]:
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
- # remove nones
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
- return super().get_list(limit=limit, params=payload)
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 new annotation.
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: A CreateAnnotationDto instance or a list of such instances.
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 or a list of ids if multiple annotations were created.
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
- respdata = await self._make_request_async_json('POST',
676
- f'{self.endpoint_base}/{resource_id}/segmentations/file',
677
- data=form)
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
- resource_id = self.get_by_id(annotation_id).resource_id
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
- except ResourceNotFoundError as e:
998
- e.set_params('annotation', {'annotation_id': annotation_id})
999
- raise e
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
- ) -> None:
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,3 +1,4 @@
1
+ """Deprecated: Use MLFlow API instead."""
1
2
  from typing import Sequence
2
3
  from ..entity_base_api import ApiConfig, BaseApi
3
4
  import httpx
@@ -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) -> 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 = [Resource(**item) for item in resources_data]
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
- ) -> str:
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.