supervisely 6.73.342__py3-none-any.whl → 6.73.344__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.
@@ -4,7 +4,10 @@
4
4
  # docs
5
5
  from __future__ import annotations
6
6
 
7
+ import os
8
+ from pathlib import Path
7
9
  from typing import (
10
+ TYPE_CHECKING,
8
11
  Any,
9
12
  Dict,
10
13
  Generator,
@@ -16,7 +19,15 @@ from typing import (
16
19
  Union,
17
20
  )
18
21
 
19
- from supervisely._utils import abs_url, compress_image_url, is_development
22
+ from tqdm import tqdm
23
+
24
+ from supervisely._utils import (
25
+ abs_url,
26
+ compress_image_url,
27
+ is_development,
28
+ run_coroutine,
29
+ )
30
+ from supervisely.annotation.annotation import Annotation
20
31
  from supervisely.api.module_api import (
21
32
  ApiField,
22
33
  ModuleApi,
@@ -24,8 +35,14 @@ from supervisely.api.module_api import (
24
35
  UpdateableModule,
25
36
  _get_single_item,
26
37
  )
38
+ from supervisely.io.json import load_json_file
27
39
  from supervisely.project.project_type import ProjectType
28
40
 
41
+ if TYPE_CHECKING:
42
+ from supervisely.project.project import ProjectMeta
43
+
44
+ from supervisely import logger
45
+
29
46
 
30
47
  class DatasetInfo(NamedTuple):
31
48
  """Represent Supervisely Dataset information.
@@ -1090,3 +1107,162 @@ class DatasetApi(UpdateableModule, RemoveableModuleApi):
1090
1107
  :rtype: bool
1091
1108
  """
1092
1109
  return self.get_info_by_name(project_id, name, parent_id=parent_id) is not None
1110
+
1111
+ def quick_import(
1112
+ self,
1113
+ dataset: Union[int, DatasetInfo],
1114
+ blob_path: str,
1115
+ offsets_path: str,
1116
+ anns: List[str],
1117
+ project_meta: Optional[ProjectMeta] = None,
1118
+ project_type: Optional[ProjectType] = None,
1119
+ log_progress: bool = True,
1120
+ ):
1121
+ """
1122
+ Quick import of images and annotations to the dataset.
1123
+ Used only for extended Supervisely format with blobs.
1124
+ Project will be automatically marked as blob project.
1125
+
1126
+ IMPORTANT: Number of annotations must be equal to the number of images in offset file.
1127
+ Image names in the offset file and annotation files must match.
1128
+
1129
+ :param dataset: Dataset ID or DatasetInfo object.
1130
+ :type dataset: Union[int, DatasetInfo]
1131
+ :param blob_path: Local path to the blob file.
1132
+ :type blob_path: str
1133
+ :param offsets_path: Local path to the offsets file.
1134
+ :type offsets_path: str
1135
+ :param anns: List of annotation paths.
1136
+ :type anns: List[str]
1137
+ :param project_meta: ProjectMeta object.
1138
+ :type project_meta: Optional[ProjectMeta], optional
1139
+ :param project_type: Project type.
1140
+ :type project_type: Optional[ProjectType], optional
1141
+ :param log_progress: If True, show progress bar.
1142
+ :type log_progress: bool, optional
1143
+
1144
+
1145
+ :Usage example:
1146
+
1147
+ .. code-block:: python
1148
+
1149
+ import supervisely as sly
1150
+ from supervisely.project.project_meta import ProjectMeta
1151
+ from supervisely.project.project_type import ProjectType
1152
+
1153
+ api = sly.Api.from_env()
1154
+
1155
+ dataset_id = 123
1156
+ workspace_id = 456
1157
+ blob_path = "/path/to/blob"
1158
+ offsets_path = "/path/to/offsets"
1159
+ project_meta_path = "/path/to/project_meta.json"
1160
+ anns = ["/path/to/ann1.json", "/path/to/ann2.json", ...]
1161
+
1162
+ # Create a new project, dataset and update its meta
1163
+ project = api.project.create(
1164
+ workspace_id,
1165
+ "Quick Import",
1166
+ type=sly.ProjectType.IMAGES,
1167
+ change_name_if_conflict=True,
1168
+ )
1169
+ dataset = api.dataset.create(project.id, "ds1")
1170
+ project_meta_json = sly.json.load_json_file(project_meta_path)
1171
+ meta = api.project.update_meta(project.id, meta=project_meta_json)
1172
+
1173
+ dataset_info = api.dataset.quick_import(
1174
+ dataset=dataset.id,
1175
+ blob_path=blob_path,
1176
+ offsets_path=offsets_path,
1177
+ anns=anns,
1178
+ project_meta=ProjectMeta(),
1179
+ project_type=ProjectType.IMAGES,
1180
+ log_progress=True
1181
+ )
1182
+
1183
+ """
1184
+ from supervisely.api.api import Api, ApiContext
1185
+ from supervisely.api.image_api import _BLOB_TAG_NAME
1186
+ from supervisely.project.project import TF_BLOB_DIR, ProjectMeta
1187
+
1188
+ def _ann_objects_generator(ann_paths, project_meta):
1189
+ for ann in ann_paths:
1190
+ ann_json = load_json_file(ann)
1191
+ yield Annotation.from_json(ann_json, project_meta)
1192
+
1193
+ self._api: Api
1194
+
1195
+ if isinstance(dataset, int):
1196
+ dataset = self.get_info_by_id(dataset)
1197
+
1198
+ project_info = self._api.project.get_info_by_id(dataset.project_id)
1199
+
1200
+ if project_meta is None:
1201
+ meta_dict = self._api.project.get_meta(dataset.project_id)
1202
+ project_meta = ProjectMeta.from_json(meta_dict)
1203
+
1204
+ if project_type is None:
1205
+ project_type = project_info.type
1206
+
1207
+ if project_type != ProjectType.IMAGES:
1208
+ raise NotImplementedError(
1209
+ f"Quick import is not implemented for project type {project_type}"
1210
+ )
1211
+
1212
+ # Set optimization context
1213
+ with ApiContext(
1214
+ api=self._api,
1215
+ project_id=dataset.project_id,
1216
+ dataset_id=dataset.id,
1217
+ project_meta=project_meta,
1218
+ ):
1219
+ dst_blob_path = os.path.join(f"/{TF_BLOB_DIR}", os.path.basename(blob_path))
1220
+ dst_offset_path = os.path.join(f"/{TF_BLOB_DIR}", os.path.basename(offsets_path))
1221
+ if log_progress:
1222
+ sizeb = os.path.getsize(blob_path) + os.path.getsize(offsets_path)
1223
+ b_progress_cb = tqdm(
1224
+ total=sizeb,
1225
+ unit="B",
1226
+ unit_scale=True,
1227
+ desc=f"Uploading blob to file storage",
1228
+ )
1229
+ else:
1230
+ b_progress_cb = None
1231
+
1232
+ self._api.file.upload_bulk_fast(
1233
+ team_id=project_info.team_id,
1234
+ src_paths=[blob_path, offsets_path],
1235
+ dst_paths=[dst_blob_path, dst_offset_path],
1236
+ progress_cb=b_progress_cb.update,
1237
+ )
1238
+
1239
+ blob_file_id = self._api.file.get_info_by_path(project_info.team_id, dst_blob_path).id
1240
+
1241
+ if log_progress:
1242
+ of_progress_cb = tqdm(desc=f"Uploading images by offsets", total=len(anns)).update
1243
+ else:
1244
+ of_progress_cb = None
1245
+
1246
+ image_info_generator = self._api.image.upload_by_offsets_generator(
1247
+ dataset=dataset,
1248
+ team_file_id=blob_file_id,
1249
+ progress_cb=of_progress_cb,
1250
+ )
1251
+
1252
+ ann_map = {Path(ann).stem: ann for ann in anns}
1253
+
1254
+ for image_info_batch in image_info_generator:
1255
+ img_ids = [img_info.id for img_info in image_info_batch]
1256
+ img_names = [img_info.name for img_info in image_info_batch]
1257
+ img_anns = [ann_map[img_name] for img_name in img_names]
1258
+ ann_objects = _ann_objects_generator(img_anns, project_meta)
1259
+ coroutine = self._api.annotation.upload_anns_async(
1260
+ image_ids=img_ids, anns=ann_objects, log_progress=log_progress
1261
+ )
1262
+ run_coroutine(coroutine)
1263
+ try:
1264
+ custom_data = self._api.project.get_custom_data(dataset.project_id)
1265
+ custom_data[_BLOB_TAG_NAME] = True
1266
+ self._api.project.update_custom_data(dataset.project_id, custom_data)
1267
+ except:
1268
+ logger.warning("Failed to set blob tag for project")
@@ -700,3 +700,87 @@ class FigureApi(RemoveableBulkModuleApi):
700
700
  raise RuntimeError("Not all geometries were downloaded")
701
701
  ordered_results = [geometries[i] for i in ids]
702
702
  return ordered_results
703
+
704
+ async def upload_geometries_batch_async(
705
+ self,
706
+ figure_ids: List[int],
707
+ geometries: List[dict],
708
+ semaphore: Optional[asyncio.Semaphore] = None,
709
+ progress_cb: Optional[Union[tqdm, Callable]] = None,
710
+ ) -> None:
711
+ """
712
+ Upload figure geometries with given figure IDs to storage asynchronously in batches.
713
+
714
+ :param figure_ids: List of figure IDs in Supervisely.
715
+ :type figure_ids: List[int]
716
+ :param geometries: List of figure geometries in Supervisely JSON format.
717
+ :type geometries: List[dict]
718
+ :param semaphore: Semaphore to limit the number of concurrent uploads.
719
+ :type semaphore: Optional[asyncio.Semaphore], optional
720
+ :param progress_cb: Progress bar to show the upload progress. Shows the number of geometries uploaded.
721
+ :type progress_cb: Union[tqdm, Callable], optional
722
+ :return: None
723
+ :rtype: None
724
+
725
+ :Usage example:
726
+
727
+ .. code-block:: python
728
+
729
+ import asyncio
730
+ import supervisely as sly
731
+
732
+ os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
733
+ os.environ['API_TOKEN'] = 'Your Supervisely API Token'
734
+ api = sly.Api.from_env()
735
+
736
+ figure_ids = [642155547, 642155548, 642155549]
737
+ geometries = [{...}, {...}, {...}] # Your geometry data
738
+
739
+ upload_coroutine = api.figure.upload_geometries_batch_async(
740
+ figure_ids,
741
+ geometries,
742
+ semaphore=asyncio.Semaphore(10),
743
+ )
744
+ sly.run_coroutine(upload_coroutine)
745
+ """
746
+ if semaphore is None:
747
+ semaphore = self._api.get_default_semaphore()
748
+
749
+ encoded_geometries = [json.dumps(geometry).encode("utf-8") for geometry in geometries]
750
+
751
+ batch_size = 100
752
+ tasks = []
753
+
754
+ for batch_ids, batch_geometries in zip(
755
+ batched(figure_ids, batch_size),
756
+ batched(encoded_geometries, batch_size),
757
+ ):
758
+ fields = []
759
+ for figure_id, geometry in zip(batch_ids, batch_geometries):
760
+ fields.append((ApiField.FIGURE_ID, str(figure_id)))
761
+ fields.append(
762
+ (
763
+ ApiField.GEOMETRY,
764
+ (str(figure_id), geometry, "application/octet-stream"),
765
+ )
766
+ )
767
+
768
+ async def upload_batch(fields, progress_cb, num):
769
+ async with semaphore:
770
+ encoder = MultipartEncoder(fields=fields)
771
+ headers = {"Content-Type": encoder.content_type}
772
+ async for _, _ in self._api.stream_async(
773
+ "figures.bulk.upload.geometry",
774
+ "POST",
775
+ data=encoder,
776
+ content=encoder.to_string(),
777
+ headers=headers,
778
+ ):
779
+ pass
780
+ if progress_cb is not None:
781
+ progress_cb.update(num)
782
+
783
+ tasks.append(upload_batch(fields, progress_cb, len(batch_ids)))
784
+
785
+ if tasks:
786
+ await asyncio.gather(*tasks)
@@ -813,7 +813,7 @@ class FileApi(ModuleApiBase):
813
813
  :type src: List[str]
814
814
  :param dst: Destination paths for Files to Team Files.
815
815
  :type dst: List[str]
816
- :param progress_cb: Function for tracking download progress.
816
+ :param progress_cb: Function for tracking upload progress.
817
817
  :type progress_cb: tqdm or callable, optional
818
818
  :return: Information about Files. See :class:`info_sequence<info_sequence>`
819
819
  :rtype: :class:`List[FileInfo]`
@@ -1546,7 +1546,7 @@ class FileApi(ModuleApiBase):
1546
1546
  sly_fs.remove_dir(temp_path)
1547
1547
  return content
1548
1548
  else:
1549
- raise FileNotFoundError(f"File not found: {remote_path}")
1549
+ raise FileNotFoundError(f"File not found in Team Files at path: {remote_path}")
1550
1550
 
1551
1551
  async def _download_async(
1552
1552
  self,