supervisely 6.73.410__py3-none-any.whl → 6.73.470__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.
Potentially problematic release.
This version of supervisely might be problematic. Click here for more details.
- supervisely/__init__.py +136 -1
- supervisely/_utils.py +81 -0
- supervisely/annotation/json_geometries_map.py +2 -0
- supervisely/annotation/label.py +80 -3
- supervisely/api/annotation_api.py +9 -9
- supervisely/api/api.py +67 -43
- supervisely/api/app_api.py +72 -5
- supervisely/api/dataset_api.py +108 -33
- supervisely/api/entity_annotation/figure_api.py +113 -49
- supervisely/api/image_api.py +82 -0
- supervisely/api/module_api.py +10 -0
- supervisely/api/nn/deploy_api.py +15 -9
- supervisely/api/nn/ecosystem_models_api.py +201 -0
- supervisely/api/nn/neural_network_api.py +12 -3
- supervisely/api/pointcloud/pointcloud_api.py +38 -0
- supervisely/api/pointcloud/pointcloud_episode_annotation_api.py +3 -0
- supervisely/api/project_api.py +213 -6
- supervisely/api/task_api.py +11 -1
- supervisely/api/video/video_annotation_api.py +4 -2
- supervisely/api/video/video_api.py +79 -1
- supervisely/api/video/video_figure_api.py +24 -11
- supervisely/api/volume/volume_api.py +38 -0
- supervisely/app/__init__.py +1 -1
- supervisely/app/content.py +14 -6
- supervisely/app/fastapi/__init__.py +1 -0
- supervisely/app/fastapi/custom_static_files.py +1 -1
- supervisely/app/fastapi/multi_user.py +88 -0
- supervisely/app/fastapi/subapp.py +175 -42
- supervisely/app/fastapi/templating.py +1 -1
- supervisely/app/fastapi/websocket.py +77 -9
- supervisely/app/singleton.py +21 -0
- supervisely/app/v1/app_service.py +18 -2
- supervisely/app/v1/constants.py +7 -1
- supervisely/app/widgets/__init__.py +11 -1
- supervisely/app/widgets/agent_selector/template.html +1 -0
- supervisely/app/widgets/card/card.py +20 -0
- supervisely/app/widgets/dataset_thumbnail/dataset_thumbnail.py +11 -2
- supervisely/app/widgets/dataset_thumbnail/template.html +3 -1
- supervisely/app/widgets/deploy_model/deploy_model.py +750 -0
- supervisely/app/widgets/dialog/dialog.py +12 -0
- supervisely/app/widgets/dialog/template.html +2 -1
- supervisely/app/widgets/dropdown_checkbox_selector/__init__.py +0 -0
- supervisely/app/widgets/dropdown_checkbox_selector/dropdown_checkbox_selector.py +87 -0
- supervisely/app/widgets/dropdown_checkbox_selector/template.html +12 -0
- supervisely/app/widgets/ecosystem_model_selector/__init__.py +0 -0
- supervisely/app/widgets/ecosystem_model_selector/ecosystem_model_selector.py +195 -0
- supervisely/app/widgets/experiment_selector/experiment_selector.py +454 -263
- supervisely/app/widgets/fast_table/fast_table.py +713 -126
- supervisely/app/widgets/fast_table/script.js +492 -95
- supervisely/app/widgets/fast_table/style.css +54 -0
- supervisely/app/widgets/fast_table/template.html +45 -5
- supervisely/app/widgets/heatmap/__init__.py +0 -0
- supervisely/app/widgets/heatmap/heatmap.py +523 -0
- supervisely/app/widgets/heatmap/script.js +378 -0
- supervisely/app/widgets/heatmap/style.css +227 -0
- supervisely/app/widgets/heatmap/template.html +21 -0
- supervisely/app/widgets/input_tag/input_tag.py +102 -15
- supervisely/app/widgets/input_tag_list/__init__.py +0 -0
- supervisely/app/widgets/input_tag_list/input_tag_list.py +274 -0
- supervisely/app/widgets/input_tag_list/template.html +70 -0
- supervisely/app/widgets/radio_table/radio_table.py +10 -2
- supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
- supervisely/app/widgets/radio_tabs/template.html +1 -0
- supervisely/app/widgets/select/select.py +6 -4
- supervisely/app/widgets/select_dataset/select_dataset.py +6 -0
- supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +83 -7
- supervisely/app/widgets/table/table.py +68 -13
- supervisely/app/widgets/tabs/tabs.py +22 -6
- supervisely/app/widgets/tabs/template.html +5 -1
- supervisely/app/widgets/transfer/style.css +3 -0
- supervisely/app/widgets/transfer/template.html +3 -1
- supervisely/app/widgets/transfer/transfer.py +48 -45
- supervisely/app/widgets/tree_select/tree_select.py +2 -0
- supervisely/convert/image/csv/csv_converter.py +24 -15
- supervisely/convert/pointcloud/nuscenes_conv/nuscenes_converter.py +43 -41
- supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_converter.py +75 -51
- supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_helper.py +137 -124
- supervisely/convert/video/video_converter.py +2 -2
- supervisely/geometry/polyline_3d.py +110 -0
- supervisely/io/env.py +161 -1
- supervisely/nn/artifacts/__init__.py +1 -1
- supervisely/nn/artifacts/artifacts.py +10 -2
- supervisely/nn/artifacts/detectron2.py +1 -0
- supervisely/nn/artifacts/hrda.py +1 -0
- supervisely/nn/artifacts/mmclassification.py +20 -0
- supervisely/nn/artifacts/mmdetection.py +5 -3
- supervisely/nn/artifacts/mmsegmentation.py +1 -0
- supervisely/nn/artifacts/ritm.py +1 -0
- supervisely/nn/artifacts/rtdetr.py +1 -0
- supervisely/nn/artifacts/unet.py +1 -0
- supervisely/nn/artifacts/utils.py +3 -0
- supervisely/nn/artifacts/yolov5.py +2 -0
- supervisely/nn/artifacts/yolov8.py +1 -0
- supervisely/nn/benchmark/semantic_segmentation/metric_provider.py +18 -18
- supervisely/nn/experiments.py +9 -0
- supervisely/nn/inference/cache.py +37 -17
- supervisely/nn/inference/gui/serving_gui_template.py +39 -13
- supervisely/nn/inference/inference.py +953 -211
- supervisely/nn/inference/inference_request.py +15 -8
- supervisely/nn/inference/instance_segmentation/instance_segmentation.py +1 -0
- supervisely/nn/inference/object_detection/object_detection.py +1 -0
- supervisely/nn/inference/predict_app/__init__.py +0 -0
- supervisely/nn/inference/predict_app/gui/__init__.py +0 -0
- supervisely/nn/inference/predict_app/gui/classes_selector.py +160 -0
- supervisely/nn/inference/predict_app/gui/gui.py +915 -0
- supervisely/nn/inference/predict_app/gui/input_selector.py +344 -0
- supervisely/nn/inference/predict_app/gui/model_selector.py +77 -0
- supervisely/nn/inference/predict_app/gui/output_selector.py +179 -0
- supervisely/nn/inference/predict_app/gui/preview.py +93 -0
- supervisely/nn/inference/predict_app/gui/settings_selector.py +881 -0
- supervisely/nn/inference/predict_app/gui/tags_selector.py +110 -0
- supervisely/nn/inference/predict_app/gui/utils.py +399 -0
- supervisely/nn/inference/predict_app/predict_app.py +176 -0
- supervisely/nn/inference/session.py +47 -39
- supervisely/nn/inference/tracking/bbox_tracking.py +5 -1
- supervisely/nn/inference/tracking/point_tracking.py +5 -1
- supervisely/nn/inference/tracking/tracker_interface.py +4 -0
- supervisely/nn/inference/uploader.py +9 -5
- supervisely/nn/model/model_api.py +44 -22
- supervisely/nn/model/prediction.py +15 -1
- supervisely/nn/model/prediction_session.py +70 -14
- supervisely/nn/prediction_dto.py +7 -0
- supervisely/nn/tracker/__init__.py +6 -8
- supervisely/nn/tracker/base_tracker.py +54 -0
- supervisely/nn/tracker/botsort/__init__.py +1 -0
- supervisely/nn/tracker/botsort/botsort_config.yaml +30 -0
- supervisely/nn/tracker/botsort/osnet_reid/__init__.py +0 -0
- supervisely/nn/tracker/botsort/osnet_reid/osnet.py +566 -0
- supervisely/nn/tracker/botsort/osnet_reid/osnet_reid_interface.py +88 -0
- supervisely/nn/tracker/botsort/tracker/__init__.py +0 -0
- supervisely/nn/tracker/{bot_sort → botsort/tracker}/basetrack.py +1 -2
- supervisely/nn/tracker/{utils → botsort/tracker}/gmc.py +51 -59
- supervisely/nn/tracker/{deep_sort/deep_sort → botsort/tracker}/kalman_filter.py +71 -33
- supervisely/nn/tracker/botsort/tracker/matching.py +202 -0
- supervisely/nn/tracker/{bot_sort/bot_sort.py → botsort/tracker/mc_bot_sort.py} +68 -81
- supervisely/nn/tracker/botsort_tracker.py +273 -0
- supervisely/nn/tracker/calculate_metrics.py +264 -0
- supervisely/nn/tracker/utils.py +273 -0
- supervisely/nn/tracker/visualize.py +520 -0
- supervisely/nn/training/gui/gui.py +152 -49
- supervisely/nn/training/gui/hyperparameters_selector.py +1 -1
- supervisely/nn/training/gui/model_selector.py +8 -6
- supervisely/nn/training/gui/train_val_splits_selector.py +144 -71
- supervisely/nn/training/gui/training_artifacts.py +3 -1
- supervisely/nn/training/train_app.py +225 -46
- supervisely/project/pointcloud_episode_project.py +12 -8
- supervisely/project/pointcloud_project.py +12 -8
- supervisely/project/project.py +221 -75
- supervisely/template/experiment/experiment.html.jinja +105 -55
- supervisely/template/experiment/experiment_generator.py +258 -112
- supervisely/template/experiment/header.html.jinja +31 -13
- supervisely/template/experiment/sly-style.css +7 -2
- supervisely/versions.json +3 -1
- supervisely/video/sampling.py +42 -20
- supervisely/video/video.py +41 -12
- supervisely/video_annotation/video_figure.py +38 -4
- supervisely/volume/stl_converter.py +2 -0
- supervisely/worker_api/agent_rpc.py +24 -1
- supervisely/worker_api/rpc_servicer.py +31 -7
- {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/METADATA +22 -14
- {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/RECORD +167 -148
- supervisely_lib/__init__.py +6 -1
- supervisely/app/widgets/experiment_selector/style.css +0 -27
- supervisely/app/widgets/experiment_selector/template.html +0 -61
- supervisely/nn/tracker/bot_sort/__init__.py +0 -21
- supervisely/nn/tracker/bot_sort/fast_reid_interface.py +0 -152
- supervisely/nn/tracker/bot_sort/matching.py +0 -127
- supervisely/nn/tracker/bot_sort/sly_tracker.py +0 -401
- supervisely/nn/tracker/deep_sort/__init__.py +0 -6
- supervisely/nn/tracker/deep_sort/deep_sort/__init__.py +0 -1
- supervisely/nn/tracker/deep_sort/deep_sort/detection.py +0 -49
- supervisely/nn/tracker/deep_sort/deep_sort/iou_matching.py +0 -81
- supervisely/nn/tracker/deep_sort/deep_sort/linear_assignment.py +0 -202
- supervisely/nn/tracker/deep_sort/deep_sort/nn_matching.py +0 -176
- supervisely/nn/tracker/deep_sort/deep_sort/track.py +0 -166
- supervisely/nn/tracker/deep_sort/deep_sort/tracker.py +0 -145
- supervisely/nn/tracker/deep_sort/deep_sort.py +0 -301
- supervisely/nn/tracker/deep_sort/generate_clip_detections.py +0 -90
- supervisely/nn/tracker/deep_sort/preprocessing.py +0 -70
- supervisely/nn/tracker/deep_sort/sly_tracker.py +0 -273
- supervisely/nn/tracker/tracker.py +0 -285
- supervisely/nn/tracker/utils/kalman_filter.py +0 -492
- supervisely/nn/tracking/__init__.py +0 -1
- supervisely/nn/tracking/boxmot.py +0 -114
- supervisely/nn/tracking/tracking.py +0 -24
- /supervisely/{nn/tracker/utils → app/widgets/deploy_model}/__init__.py +0 -0
- {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/LICENSE +0 -0
- {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/WHEEL +0 -0
- {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.410.dist-info → supervisely-6.73.470.dist-info}/top_level.txt +0 -0
supervisely/api/project_api.py
CHANGED
|
@@ -12,10 +12,12 @@ from typing import (
|
|
|
12
12
|
Any,
|
|
13
13
|
Callable,
|
|
14
14
|
Dict,
|
|
15
|
+
Generator,
|
|
15
16
|
List,
|
|
16
17
|
Literal,
|
|
17
18
|
NamedTuple,
|
|
18
19
|
Optional,
|
|
20
|
+
Tuple,
|
|
19
21
|
Union,
|
|
20
22
|
)
|
|
21
23
|
|
|
@@ -38,12 +40,14 @@ from supervisely.annotation.annotation import TagCollection
|
|
|
38
40
|
from supervisely.annotation.obj_class import ObjClass
|
|
39
41
|
from supervisely.annotation.obj_class_collection import ObjClassCollection
|
|
40
42
|
from supervisely.annotation.tag_meta import TagMeta, TagValueType
|
|
43
|
+
from supervisely.api.dataset_api import DatasetInfo
|
|
41
44
|
from supervisely.api.module_api import (
|
|
42
45
|
ApiField,
|
|
43
46
|
CloneableModuleApi,
|
|
44
47
|
RemoveableModuleApi,
|
|
45
48
|
UpdateableModule,
|
|
46
49
|
)
|
|
50
|
+
from supervisely.io.env import upload_count, uploaded_ids
|
|
47
51
|
from supervisely.io.json import dump_json_file, load_json_file
|
|
48
52
|
from supervisely.project.project_meta import ProjectMeta
|
|
49
53
|
from supervisely.project.project_meta import ProjectMetaJsonFields as MetaJsonF
|
|
@@ -264,9 +268,10 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
|
|
|
264
268
|
|
|
265
269
|
def get_list(
|
|
266
270
|
self,
|
|
267
|
-
workspace_id: int,
|
|
271
|
+
workspace_id: Optional[int] = None,
|
|
268
272
|
filters: Optional[List[Dict[str, str]]] = None,
|
|
269
273
|
fields: List[str] = [],
|
|
274
|
+
team_id: Optional[int] = None,
|
|
270
275
|
) -> List[ProjectInfo]:
|
|
271
276
|
"""
|
|
272
277
|
List of Projects in the given Workspace.
|
|
@@ -275,12 +280,13 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
|
|
|
275
280
|
If you need version information, use :func:`get_info_by_id`.
|
|
276
281
|
|
|
277
282
|
:param workspace_id: Workspace ID in which the Projects are located.
|
|
278
|
-
:type workspace_id: int
|
|
283
|
+
:type workspace_id: int, optional
|
|
279
284
|
:param filters: List of params to sort output Projects.
|
|
280
285
|
:type filters: List[dict], optional
|
|
281
286
|
:param fields: The list of api fields which will be returned with the response. You must specify all fields you want to receive, not just additional ones.
|
|
282
287
|
:type fields: List[str]
|
|
283
|
-
|
|
288
|
+
:param team_id: Team ID in which the Projects are located.
|
|
289
|
+
:type team_id: int, optional
|
|
284
290
|
:return: List of all projects with information for the given Workspace. See :class:`info_sequence<info_sequence>`
|
|
285
291
|
:rtype: :class: `List[ProjectInfo]`
|
|
286
292
|
:Usage example:
|
|
@@ -357,6 +363,11 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
|
|
|
357
363
|
# ]
|
|
358
364
|
|
|
359
365
|
"""
|
|
366
|
+
if team_id is not None and workspace_id is not None:
|
|
367
|
+
raise ValueError(
|
|
368
|
+
"team_id and workspace_id cannot be used together. Please provide only one of them."
|
|
369
|
+
)
|
|
370
|
+
|
|
360
371
|
method = "projects.list"
|
|
361
372
|
|
|
362
373
|
debug_message = "While getting list of projects, the following fields are not available: "
|
|
@@ -367,11 +378,33 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
|
|
|
367
378
|
self.debug_messages_sent["get_list_versions"] = True
|
|
368
379
|
logger.debug(debug_message + "version. ")
|
|
369
380
|
|
|
381
|
+
default_fields = [
|
|
382
|
+
ApiField.ID,
|
|
383
|
+
ApiField.WORKSPACE_ID,
|
|
384
|
+
ApiField.TITLE,
|
|
385
|
+
ApiField.DESCRIPTION,
|
|
386
|
+
ApiField.SIZE,
|
|
387
|
+
ApiField.README,
|
|
388
|
+
ApiField.TYPE,
|
|
389
|
+
ApiField.CREATED_AT,
|
|
390
|
+
ApiField.UPDATED_AT,
|
|
391
|
+
ApiField.CUSTOM_DATA,
|
|
392
|
+
ApiField.GROUP_ID,
|
|
393
|
+
ApiField.CREATED_BY_ID[0][0],
|
|
394
|
+
]
|
|
395
|
+
|
|
396
|
+
if fields:
|
|
397
|
+
merged_fields = list(set(default_fields + fields))
|
|
398
|
+
fields = list(dict.fromkeys(merged_fields))
|
|
399
|
+
|
|
370
400
|
data = {
|
|
371
|
-
ApiField.WORKSPACE_ID: workspace_id,
|
|
372
401
|
ApiField.FILTER: filters or [],
|
|
373
402
|
ApiField.FIELDS: fields,
|
|
374
403
|
}
|
|
404
|
+
if workspace_id is not None:
|
|
405
|
+
data[ApiField.WORKSPACE_ID] = workspace_id
|
|
406
|
+
if team_id is not None:
|
|
407
|
+
data[ApiField.GROUP_ID] = team_id
|
|
375
408
|
|
|
376
409
|
return self.get_list_all_pages(method, data)
|
|
377
410
|
|
|
@@ -2129,7 +2162,7 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
|
|
|
2129
2162
|
# reference_image_url = None,
|
|
2130
2163
|
# custom_data = None,
|
|
2131
2164
|
# backup_archive = None,
|
|
2132
|
-
#
|
|
2165
|
+
# team_id = 1,
|
|
2133
2166
|
# import_settings = {},
|
|
2134
2167
|
# ),
|
|
2135
2168
|
# ProjectInfo(id = 23,
|
|
@@ -2147,7 +2180,7 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
|
|
|
2147
2180
|
# reference_image_url = None,
|
|
2148
2181
|
# custom_data = None,
|
|
2149
2182
|
# backup_archive = None),
|
|
2150
|
-
#
|
|
2183
|
+
# team_id = 1,
|
|
2151
2184
|
# import_settings = {},
|
|
2152
2185
|
# )
|
|
2153
2186
|
# ]
|
|
@@ -2455,6 +2488,7 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
|
|
|
2455
2488
|
request_body = {
|
|
2456
2489
|
ApiField.PROJECT_ID: project_id,
|
|
2457
2490
|
ApiField.LIMIT: limit,
|
|
2491
|
+
ApiField.UNIQUE_ITEMS: limit, # the same as limit, but for diverse search
|
|
2458
2492
|
}
|
|
2459
2493
|
|
|
2460
2494
|
if dataset_id is not None:
|
|
@@ -2526,3 +2560,176 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
|
|
|
2526
2560
|
api.project.calculate_embeddings(project_id)
|
|
2527
2561
|
"""
|
|
2528
2562
|
self._api.post("embeddings.calculate-project-embeddings", {ApiField.PROJECT_ID: id})
|
|
2563
|
+
|
|
2564
|
+
def recreate_structure_generator(
|
|
2565
|
+
self,
|
|
2566
|
+
src_project_id: int,
|
|
2567
|
+
dst_project_id: Optional[int] = None,
|
|
2568
|
+
dst_project_name: Optional[str] = None,
|
|
2569
|
+
) -> Generator[Tuple[DatasetInfo, DatasetInfo], None, None]:
|
|
2570
|
+
"""This method can be used to recreate a project with hierarchial datasets (without the data itself) and
|
|
2571
|
+
yields the tuple of source and destination DatasetInfo objects.
|
|
2572
|
+
|
|
2573
|
+
:param src_project_id: Source project ID
|
|
2574
|
+
:type src_project_id: int
|
|
2575
|
+
:param dst_project_id: Destination project ID
|
|
2576
|
+
:type dst_project_id: int, optional
|
|
2577
|
+
:param dst_project_name: Name of the destination project. If `dst_project_id` is None, a new project will be created with this name. If `dst_project_id` is provided, this parameter will be ignored.
|
|
2578
|
+
:type dst_project_name: str, optional
|
|
2579
|
+
|
|
2580
|
+
:return: Generator of tuples of source and destination DatasetInfo objects
|
|
2581
|
+
:rtype: Generator[Tuple[DatasetInfo, DatasetInfo], None, None]
|
|
2582
|
+
|
|
2583
|
+
:Usage example:
|
|
2584
|
+
|
|
2585
|
+
.. code-block:: python
|
|
2586
|
+
|
|
2587
|
+
import supervisely as sly
|
|
2588
|
+
|
|
2589
|
+
api = sly.Api.from_env()
|
|
2590
|
+
|
|
2591
|
+
src_project_id = 123
|
|
2592
|
+
dst_project_id = api.project.create("new_project", "images").id
|
|
2593
|
+
|
|
2594
|
+
for src_ds, dst_ds in api.project.recreate_structure_generator(src_project_id, dst_project_id):
|
|
2595
|
+
print(f"Recreated dataset {src_ds.id} -> {dst_ds.id}")
|
|
2596
|
+
# Implement your logic here to process the datasets.
|
|
2597
|
+
"""
|
|
2598
|
+
if dst_project_id is None:
|
|
2599
|
+
src_project_info = self._api.project.get_info_by_id(src_project_id)
|
|
2600
|
+
dst_project_info = self._api.project.create(
|
|
2601
|
+
src_project_info.workspace_id,
|
|
2602
|
+
dst_project_name or f"Recreation of {src_project_info.name}",
|
|
2603
|
+
src_project_info.type,
|
|
2604
|
+
src_project_info.description,
|
|
2605
|
+
change_name_if_conflict=True,
|
|
2606
|
+
)
|
|
2607
|
+
dst_project_id = dst_project_info.id
|
|
2608
|
+
|
|
2609
|
+
datasets = self._api.dataset.get_list(src_project_id, recursive=True, include_custom_data=True)
|
|
2610
|
+
src_to_dst_ids = {}
|
|
2611
|
+
|
|
2612
|
+
for src_dataset_info in datasets:
|
|
2613
|
+
dst_dataset_info = self._api.dataset.create(
|
|
2614
|
+
dst_project_id,
|
|
2615
|
+
src_dataset_info.name,
|
|
2616
|
+
description=src_dataset_info.description,
|
|
2617
|
+
parent_id=src_to_dst_ids.get(src_dataset_info.parent_id),
|
|
2618
|
+
custom_data=src_dataset_info.custom_data,
|
|
2619
|
+
)
|
|
2620
|
+
src_to_dst_ids[src_dataset_info.id] = dst_dataset_info.id
|
|
2621
|
+
|
|
2622
|
+
yield src_dataset_info, dst_dataset_info
|
|
2623
|
+
|
|
2624
|
+
def recreate_structure(
|
|
2625
|
+
self,
|
|
2626
|
+
src_project_id: int,
|
|
2627
|
+
dst_project_id: Optional[int] = None,
|
|
2628
|
+
dst_project_name: Optional[str] = None,
|
|
2629
|
+
) -> Tuple[List[DatasetInfo], List[DatasetInfo]]:
|
|
2630
|
+
"""This method can be used to recreate a project with hierarchial datasets (without the data itself).
|
|
2631
|
+
|
|
2632
|
+
:param src_project_id: Source project ID
|
|
2633
|
+
:type src_project_id: int
|
|
2634
|
+
:param dst_project_id: Destination project ID
|
|
2635
|
+
:type dst_project_id: int, optional
|
|
2636
|
+
:param dst_project_name: Name of the destination project. If `dst_project_id` is None, a new project will be created with this name. If `dst_project_id` is provided, this parameter will be ignored.
|
|
2637
|
+
:type dst_project_name: str, optional
|
|
2638
|
+
|
|
2639
|
+
:return: Destination project ID
|
|
2640
|
+
:rtype: int
|
|
2641
|
+
|
|
2642
|
+
:Usage example:
|
|
2643
|
+
|
|
2644
|
+
.. code-block:: python
|
|
2645
|
+
|
|
2646
|
+
import supervisely as sly
|
|
2647
|
+
|
|
2648
|
+
api = sly.Api.from_env()
|
|
2649
|
+
|
|
2650
|
+
src_project_id = 123
|
|
2651
|
+
dst_project_name = "New Project"
|
|
2652
|
+
|
|
2653
|
+
dst_project_id = api.project.recreate_structure(src_project_id, dst_project_name=dst_project_name)
|
|
2654
|
+
print(f"Recreated project {src_project_id} -> {dst_project_id}")
|
|
2655
|
+
"""
|
|
2656
|
+
infos = []
|
|
2657
|
+
for src_info, dst_info in self.recreate_structure_generator(
|
|
2658
|
+
src_project_id, dst_project_id, dst_project_name
|
|
2659
|
+
):
|
|
2660
|
+
infos.append((src_info, dst_info))
|
|
2661
|
+
|
|
2662
|
+
return infos
|
|
2663
|
+
|
|
2664
|
+
def add_import_history(self, id: int, task_id: int) -> None:
|
|
2665
|
+
"""
|
|
2666
|
+
Adds import history to project info. Gets task info and adds it to project custom data.
|
|
2667
|
+
|
|
2668
|
+
:param id: Project ID
|
|
2669
|
+
:type id: int
|
|
2670
|
+
:param task_id: Task ID
|
|
2671
|
+
:type task_id: int
|
|
2672
|
+
:return: None
|
|
2673
|
+
:rtype: :class:`NoneType`
|
|
2674
|
+
:Usage example:
|
|
2675
|
+
.. code-block:: python
|
|
2676
|
+
import os
|
|
2677
|
+
from dotenv import load_dotenv
|
|
2678
|
+
|
|
2679
|
+
import supervisely as sly
|
|
2680
|
+
|
|
2681
|
+
# Load secrets and create API object from .env file (recommended)
|
|
2682
|
+
# Learn more here: https://developer.supervisely.com/getting-started/basics-of-authentication
|
|
2683
|
+
load_dotenv(os.path.expanduser("~/supervisely.env"))
|
|
2684
|
+
api = sly.Api.from_env()
|
|
2685
|
+
|
|
2686
|
+
project_id = 123
|
|
2687
|
+
task_id = 456
|
|
2688
|
+
api.project.add_import_history(project_id, task_id)
|
|
2689
|
+
"""
|
|
2690
|
+
|
|
2691
|
+
task_info = self._api.task.get_info_by_id(task_id)
|
|
2692
|
+
module_id = task_info.get("meta", {}).get("app", {}).get("moduleId")
|
|
2693
|
+
slug = None
|
|
2694
|
+
if module_id is not None:
|
|
2695
|
+
module_info = self._api.app.get_ecosystem_module_info(module_id)
|
|
2696
|
+
slug = module_info.slug
|
|
2697
|
+
|
|
2698
|
+
items_count = upload_count()
|
|
2699
|
+
items_count = {int(k): v for k, v in items_count.items()}
|
|
2700
|
+
uploaded_images = uploaded_ids()
|
|
2701
|
+
uploaded_images = {int(k): v for k, v in uploaded_images.items()}
|
|
2702
|
+
total_items = sum(items_count.values()) if len(items_count) > 0 else 0
|
|
2703
|
+
app = task_info.get("meta", {}).get("app")
|
|
2704
|
+
app_name = app.get("name") if app else None
|
|
2705
|
+
app_version = app.get("version") if app else None
|
|
2706
|
+
data = {
|
|
2707
|
+
"task_id": task_id,
|
|
2708
|
+
"app": {"name": app_name, "version": app_version},
|
|
2709
|
+
"slug": slug,
|
|
2710
|
+
"status": task_info.get(ApiField.STATUS),
|
|
2711
|
+
"user_id": task_info.get(ApiField.USER_ID),
|
|
2712
|
+
"team_id": task_info.get(ApiField.TEAM_ID),
|
|
2713
|
+
"timestamp": datetime.now().strftime("%Y-%m-%dT%H:%M:%S"),
|
|
2714
|
+
"source_state": task_info.get("settings", {}).get("message", {}).get("state"),
|
|
2715
|
+
"items_count": total_items,
|
|
2716
|
+
"datasets": [
|
|
2717
|
+
{
|
|
2718
|
+
"id": ds,
|
|
2719
|
+
"items_count": items_count[ds],
|
|
2720
|
+
"uploaded_images": uploaded_images.get(ds, []),
|
|
2721
|
+
}
|
|
2722
|
+
for ds in items_count.keys()
|
|
2723
|
+
],
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
project_info = self.get_info_by_id(id)
|
|
2727
|
+
|
|
2728
|
+
custom_data = project_info.custom_data or {}
|
|
2729
|
+
if "import_history" not in custom_data:
|
|
2730
|
+
custom_data["import_history"] = {"tasks": []}
|
|
2731
|
+
if "tasks" not in custom_data["import_history"]:
|
|
2732
|
+
custom_data["import_history"]["tasks"] = []
|
|
2733
|
+
custom_data["import_history"]["tasks"].append(data)
|
|
2734
|
+
|
|
2735
|
+
self.edit_info(id, custom_data=custom_data)
|
supervisely/api/task_api.py
CHANGED
|
@@ -24,6 +24,7 @@ from supervisely.api.module_api import (
|
|
|
24
24
|
WaitingTimeExceeded,
|
|
25
25
|
)
|
|
26
26
|
from supervisely.collection.str_enum import StrEnum
|
|
27
|
+
from supervisely.io.env import app_categories
|
|
27
28
|
from supervisely.io.fs import (
|
|
28
29
|
ensure_base_path,
|
|
29
30
|
get_file_hash,
|
|
@@ -652,6 +653,8 @@ class TaskApi(ModuleApiBase, ModuleWithStatus):
|
|
|
652
653
|
project_preview: Optional[str] = None,
|
|
653
654
|
) -> Dict:
|
|
654
655
|
"""set_output_project"""
|
|
656
|
+
if "import" in app_categories():
|
|
657
|
+
self._api.project.add_import_history(project_id, task_id)
|
|
655
658
|
if project_name is None:
|
|
656
659
|
project = self._api.project.get_info_by_id(project_id, raise_error=True)
|
|
657
660
|
project_name = project.name
|
|
@@ -1007,11 +1010,12 @@ class TaskApi(ModuleApiBase, ModuleWithStatus):
|
|
|
1007
1010
|
Example of experiment_info:
|
|
1008
1011
|
|
|
1009
1012
|
experiment_info = {
|
|
1010
|
-
'experiment_name': '
|
|
1013
|
+
'experiment_name': '247 Lemons RT-DETRv2-M',
|
|
1011
1014
|
'framework_name': 'RT-DETRv2',
|
|
1012
1015
|
'model_name': 'RT-DETRv2-M',
|
|
1013
1016
|
'task_type': 'object detection',
|
|
1014
1017
|
'project_id': 76,
|
|
1018
|
+
'project_version': {'id': 222, 'version': 4},
|
|
1015
1019
|
'task_id': 247,
|
|
1016
1020
|
'model_files': {'config': 'model_config.yml'},
|
|
1017
1021
|
'checkpoints': ['checkpoints/best.pth', 'checkpoints/checkpoint0025.pth', 'checkpoints/checkpoint0050.pth', 'checkpoints/last.pth'],
|
|
@@ -1022,10 +1026,13 @@ class TaskApi(ModuleApiBase, ModuleWithStatus):
|
|
|
1022
1026
|
'train_val_split': 'train_val_split.json',
|
|
1023
1027
|
'train_size': 4,
|
|
1024
1028
|
'val_size': 2,
|
|
1029
|
+
'train_collection_id': 530,
|
|
1030
|
+
'val_collection_id': 531,
|
|
1025
1031
|
'hyperparameters': 'hyperparameters.yaml',
|
|
1026
1032
|
'hyperparameters_id': 45234,
|
|
1027
1033
|
'artifacts_dir': '/experiments/76_Lemons/247_RT-DETRv2/',
|
|
1028
1034
|
'datetime': '2025-01-22 18:13:43',
|
|
1035
|
+
'experiment_report_id': 87654,
|
|
1029
1036
|
'evaluation_report_id': 12961,
|
|
1030
1037
|
'evaluation_report_link': 'https://app.supervisely.com/model-benchmark?id=12961',
|
|
1031
1038
|
'evaluation_metrics': {
|
|
@@ -1046,6 +1053,9 @@ class TaskApi(ModuleApiBase, ModuleWithStatus):
|
|
|
1046
1053
|
'type': 'tensorboard',
|
|
1047
1054
|
'link': '/experiments/76_Lemons/247_RT-DETRv2/logs/'
|
|
1048
1055
|
},
|
|
1056
|
+
# These fields are present only in task_info
|
|
1057
|
+
'project_preview': 'https://app.supervisely.com/...',
|
|
1058
|
+
'has_report': True,
|
|
1049
1059
|
}
|
|
1050
1060
|
"""
|
|
1051
1061
|
output = {
|
|
@@ -236,11 +236,13 @@ class VideoAnnotationAPI(EntityAnnotationAPI):
|
|
|
236
236
|
dst_project_meta = ProjectMeta.from_json(
|
|
237
237
|
self._api.project.get_meta(dst_dataset_info.project_id)
|
|
238
238
|
)
|
|
239
|
-
for src_ids_batch, dst_ids_batch in batched(
|
|
239
|
+
for src_ids_batch, dst_ids_batch in zip(batched(src_video_ids), batched(dst_video_ids)):
|
|
240
240
|
ann_jsons = self.download_bulk(src_dataset_id, src_ids_batch)
|
|
241
241
|
for dst_id, ann_json in zip(dst_ids_batch, ann_jsons):
|
|
242
242
|
try:
|
|
243
|
-
ann = VideoAnnotation.from_json(
|
|
243
|
+
ann = VideoAnnotation.from_json(
|
|
244
|
+
ann_json, dst_project_meta, key_id_map=KeyIdMap()
|
|
245
|
+
)
|
|
244
246
|
except Exception as e:
|
|
245
247
|
raise RuntimeError("Failed to validate Annotation") from e
|
|
246
248
|
self.append(dst_id, ann)
|
|
@@ -5,6 +5,7 @@ import asyncio
|
|
|
5
5
|
import datetime
|
|
6
6
|
import json
|
|
7
7
|
import os
|
|
8
|
+
import re
|
|
8
9
|
import urllib.parse
|
|
9
10
|
from functools import partial
|
|
10
11
|
from typing import (
|
|
@@ -23,7 +24,11 @@ from typing import (
|
|
|
23
24
|
import aiofiles
|
|
24
25
|
from numerize.numerize import numerize
|
|
25
26
|
from requests import Response
|
|
26
|
-
from requests_toolbelt import
|
|
27
|
+
from requests_toolbelt import (
|
|
28
|
+
MultipartDecoder,
|
|
29
|
+
MultipartEncoder,
|
|
30
|
+
MultipartEncoderMonitor,
|
|
31
|
+
)
|
|
27
32
|
from tqdm import tqdm
|
|
28
33
|
|
|
29
34
|
import supervisely.io.fs as sly_fs
|
|
@@ -1186,6 +1191,41 @@ class VideoApi(RemoveableBulkModuleApi):
|
|
|
1186
1191
|
if progress_cb is not None:
|
|
1187
1192
|
progress_cb(len(chunk))
|
|
1188
1193
|
|
|
1194
|
+
def download_frames(
|
|
1195
|
+
self, video_id: int, frames: List[int], paths: List[str], progress_cb=None
|
|
1196
|
+
) -> None:
|
|
1197
|
+
endpoint = "videos.bulk.download-frame"
|
|
1198
|
+
response: Response = self._api.get(
|
|
1199
|
+
endpoint,
|
|
1200
|
+
params={},
|
|
1201
|
+
data={ApiField.VIDEO_ID: video_id, ApiField.FRAMES: frames},
|
|
1202
|
+
stream=True,
|
|
1203
|
+
)
|
|
1204
|
+
response.raise_for_status()
|
|
1205
|
+
|
|
1206
|
+
files = {frame_n: None for frame_n in frames}
|
|
1207
|
+
file_paths = {frame_n: path for frame_n, path in zip(frames, paths)}
|
|
1208
|
+
|
|
1209
|
+
try:
|
|
1210
|
+
decoder = MultipartDecoder.from_response(response)
|
|
1211
|
+
for part in decoder.parts:
|
|
1212
|
+
content_utf8 = part.headers[b"Content-Disposition"].decode("utf-8")
|
|
1213
|
+
# Find name="1245" preceded by a whitespace, semicolon or beginning of line.
|
|
1214
|
+
# The regex has 2 capture group: one for the prefix and one for the actual name value.
|
|
1215
|
+
frame_n = int(re.findall(r'(^|[\s;])name="(\d*)"', content_utf8)[0][1])
|
|
1216
|
+
if files[frame_n] is None:
|
|
1217
|
+
file_path = file_paths[frame_n]
|
|
1218
|
+
files[frame_n] = open(file_path, "wb")
|
|
1219
|
+
if progress_cb is not None:
|
|
1220
|
+
progress_cb(1)
|
|
1221
|
+
f = files[frame_n]
|
|
1222
|
+
f.write(part.content)
|
|
1223
|
+
|
|
1224
|
+
finally:
|
|
1225
|
+
for f in files.values():
|
|
1226
|
+
if f is not None:
|
|
1227
|
+
f.close()
|
|
1228
|
+
|
|
1189
1229
|
def download_range_by_id(
|
|
1190
1230
|
self,
|
|
1191
1231
|
id: int,
|
|
@@ -2610,3 +2650,41 @@ class VideoApi(RemoveableBulkModuleApi):
|
|
|
2610
2650
|
|
|
2611
2651
|
tasks.append(task)
|
|
2612
2652
|
await asyncio.gather(*tasks)
|
|
2653
|
+
|
|
2654
|
+
def rename(
|
|
2655
|
+
self,
|
|
2656
|
+
id: int,
|
|
2657
|
+
name: str,
|
|
2658
|
+
) -> VideoInfo:
|
|
2659
|
+
"""Renames Video with given ID to a new name.
|
|
2660
|
+
|
|
2661
|
+
:param id: Video ID in Supervisely.
|
|
2662
|
+
:type id: int
|
|
2663
|
+
:param name: New Video name.
|
|
2664
|
+
:type name: str
|
|
2665
|
+
:return: Information about updated Video.
|
|
2666
|
+
:rtype: :class:`VideoInfo`
|
|
2667
|
+
|
|
2668
|
+
:Usage example:
|
|
2669
|
+
|
|
2670
|
+
.. code-block:: python
|
|
2671
|
+
|
|
2672
|
+
import supervisely as sly
|
|
2673
|
+
|
|
2674
|
+
api = sly.Api.from_env()
|
|
2675
|
+
os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
|
|
2676
|
+
os.environ['API_TOKEN'] = 'Your Supervisely API Token'
|
|
2677
|
+
|
|
2678
|
+
video_id = 123456
|
|
2679
|
+
new_video_name = "VID_3333_new.mp4"
|
|
2680
|
+
|
|
2681
|
+
api.video.rename(id=video_id, name=new_video_name)
|
|
2682
|
+
"""
|
|
2683
|
+
|
|
2684
|
+
data = {
|
|
2685
|
+
ApiField.ID: id,
|
|
2686
|
+
ApiField.NAME: name,
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
response = self._api.post("images.editInfo", data)
|
|
2690
|
+
return self._convert_json_info(response.json())
|
|
@@ -10,7 +10,7 @@ from supervisely.api.module_api import ApiField
|
|
|
10
10
|
from supervisely.geometry.geometry import Geometry
|
|
11
11
|
from supervisely.video_annotation.key_id_map import KeyIdMap
|
|
12
12
|
from supervisely.video_annotation.video_figure import VideoFigure
|
|
13
|
-
|
|
13
|
+
from supervisely.annotation.label import LabelingStatus
|
|
14
14
|
|
|
15
15
|
class VideoFigureApi(FigureApi):
|
|
16
16
|
"""
|
|
@@ -26,6 +26,7 @@ class VideoFigureApi(FigureApi):
|
|
|
26
26
|
geometry_type: str,
|
|
27
27
|
track_id: Optional[int] = None,
|
|
28
28
|
meta: Optional[dict] = None,
|
|
29
|
+
status: Optional[LabelingStatus] = None,
|
|
29
30
|
) -> int:
|
|
30
31
|
"""
|
|
31
32
|
Create new VideoFigure for given frame in given video ID.
|
|
@@ -42,6 +43,10 @@ class VideoFigureApi(FigureApi):
|
|
|
42
43
|
:type geometry_type: str
|
|
43
44
|
:param track_id: int, optional.
|
|
44
45
|
:type track_id: int, optional
|
|
46
|
+
:param meta: Meta data for VideoFigure.
|
|
47
|
+
:type meta: dict, optional
|
|
48
|
+
:param status: Labeling status. Specifies if the VideoFigure was created by NN model, manually or created by NN and then manually corrected.
|
|
49
|
+
:type status: LabelingStatus, optional
|
|
45
50
|
:return: New figure ID
|
|
46
51
|
:rtype: :class:`int`
|
|
47
52
|
:Usage example:
|
|
@@ -64,13 +69,16 @@ class VideoFigureApi(FigureApi):
|
|
|
64
69
|
"""
|
|
65
70
|
if meta is None:
|
|
66
71
|
meta = {}
|
|
72
|
+
meta = {**(meta or {}), ApiField.FRAME: frame_index}
|
|
73
|
+
|
|
67
74
|
return super().create(
|
|
68
75
|
video_id,
|
|
69
76
|
object_id,
|
|
70
|
-
|
|
77
|
+
meta,
|
|
71
78
|
geometry_json,
|
|
72
79
|
geometry_type,
|
|
73
80
|
track_id,
|
|
81
|
+
status=status,
|
|
74
82
|
)
|
|
75
83
|
|
|
76
84
|
def append_bulk(self, video_id: int, figures: List[VideoFigure], key_id_map: KeyIdMap) -> None:
|
|
@@ -115,13 +123,15 @@ class VideoFigureApi(FigureApi):
|
|
|
115
123
|
|
|
116
124
|
self._append_bulk(video_id, figures_json, keys, key_id_map)
|
|
117
125
|
|
|
118
|
-
def update(self, figure_id: int, geometry: Geometry) -> None:
|
|
126
|
+
def update(self, figure_id: int, geometry: Geometry, status: Optional[LabelingStatus] = None) -> None:
|
|
119
127
|
"""Updates figure feometry with given ID in Supervisely with new Geometry object.
|
|
120
128
|
|
|
121
129
|
:param figure_id: ID of the figure to update
|
|
122
130
|
:type figure_id: int
|
|
123
131
|
:param geometry: Supervisely Gepmetry object
|
|
124
132
|
:type geometry: Geometry
|
|
133
|
+
:param status: Labeling status. Specifies if the VideoFigure was created by NN model, manually or created by NN and then manually corrected.
|
|
134
|
+
:type status: LabelingStatus, optional
|
|
125
135
|
:Usage example:
|
|
126
136
|
|
|
127
137
|
.. code-block:: python
|
|
@@ -141,13 +151,17 @@ class VideoFigureApi(FigureApi):
|
|
|
141
151
|
|
|
142
152
|
api.video.figure.update(figure_id, new_geometry)
|
|
143
153
|
"""
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
154
|
+
payload = {
|
|
155
|
+
ApiField.ID: figure_id,
|
|
156
|
+
ApiField.GEOMETRY: geometry.to_json(),
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if status is not None:
|
|
160
|
+
nn_created,nn_updated = LabelingStatus.to_flags(status)
|
|
161
|
+
payload[ApiField.NN_CREATED] = nn_created
|
|
162
|
+
payload[ApiField.NN_UPDATED] = nn_updated
|
|
163
|
+
|
|
164
|
+
self._api.post("figures.editInfo", payload)
|
|
151
165
|
|
|
152
166
|
def download(
|
|
153
167
|
self, dataset_id: int, video_ids: List[int] = None, skip_geometry: bool = False, **kwargs
|
|
@@ -161,7 +175,6 @@ class VideoFigureApi(FigureApi):
|
|
|
161
175
|
:type video_ids: List[int], optional
|
|
162
176
|
:param skip_geometry: Skip the download of figure geometry. May be useful for a significant api request speed increase in the large datasets.
|
|
163
177
|
:type skip_geometry: bool
|
|
164
|
-
|
|
165
178
|
:return: A dictionary where keys are video IDs and values are lists of figures.
|
|
166
179
|
:rtype: :class: `Dict[int, List[FigureInfo]]`
|
|
167
180
|
"""
|
|
@@ -1455,3 +1455,41 @@ class VolumeApi(RemoveableBulkModuleApi):
|
|
|
1455
1455
|
)
|
|
1456
1456
|
tasks.append(task)
|
|
1457
1457
|
await asyncio.gather(*tasks)
|
|
1458
|
+
|
|
1459
|
+
def rename(
|
|
1460
|
+
self,
|
|
1461
|
+
id: int,
|
|
1462
|
+
name: str,
|
|
1463
|
+
) -> VolumeInfo:
|
|
1464
|
+
"""Renames Volume with given ID to a new name.
|
|
1465
|
+
|
|
1466
|
+
:param id: Volume ID in Supervisely.
|
|
1467
|
+
:type id: int
|
|
1468
|
+
:param name: New Volume name.
|
|
1469
|
+
:type name: str
|
|
1470
|
+
:return: Information about updated Volume.
|
|
1471
|
+
:rtype: :class:`VolumeInfo`
|
|
1472
|
+
|
|
1473
|
+
:Usage example:
|
|
1474
|
+
|
|
1475
|
+
.. code-block:: python
|
|
1476
|
+
|
|
1477
|
+
import supervisely as sly
|
|
1478
|
+
|
|
1479
|
+
api = sly.Api.from_env()
|
|
1480
|
+
os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
|
|
1481
|
+
os.environ['API_TOKEN'] = 'Your Supervisely API Token'
|
|
1482
|
+
|
|
1483
|
+
volume_id = 123456
|
|
1484
|
+
new_volume_name = "3333_new.nrrd"
|
|
1485
|
+
|
|
1486
|
+
api.volume.rename(id=volume_id, name=new_volume_name)
|
|
1487
|
+
"""
|
|
1488
|
+
|
|
1489
|
+
data = {
|
|
1490
|
+
ApiField.ID: id,
|
|
1491
|
+
ApiField.NAME: name,
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
response = self._api.post("images.editInfo", data)
|
|
1495
|
+
return self._convert_json_info(response.json())
|
supervisely/app/__init__.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from fastapi import FastAPI
|
|
2
2
|
from supervisely.app.content import StateJson, DataJson
|
|
3
3
|
from supervisely.app.content import get_data_dir, get_synced_data_dir
|
|
4
|
-
from supervisely.app.fastapi.subapp import call_on_autostart
|
|
4
|
+
from supervisely.app.fastapi.subapp import call_on_autostart, session_user_api
|
|
5
5
|
import supervisely.app.fastapi as fastapi
|
|
6
6
|
import supervisely.app.widgets as widgets
|
|
7
7
|
import supervisely.app.development as development
|
supervisely/app/content.py
CHANGED
|
@@ -11,6 +11,7 @@ import threading
|
|
|
11
11
|
import time
|
|
12
12
|
import traceback
|
|
13
13
|
from concurrent.futures import ThreadPoolExecutor
|
|
14
|
+
from typing import Optional, Union
|
|
14
15
|
|
|
15
16
|
import jsonpatch
|
|
16
17
|
from fastapi import Request
|
|
@@ -109,16 +110,22 @@ class _PatchableJson(dict):
|
|
|
109
110
|
patch.apply(self._last, in_place=True)
|
|
110
111
|
self._last = copy.deepcopy(self._last)
|
|
111
112
|
|
|
112
|
-
async def synchronize_changes(self):
|
|
113
|
+
async def synchronize_changes(self, user_id: Optional[Union[int, str]] = None):
|
|
113
114
|
patch = self._get_patch()
|
|
114
115
|
await self._apply_patch(patch)
|
|
115
|
-
await self._ws.broadcast(self.get_changes(patch))
|
|
116
|
+
await self._ws.broadcast(self.get_changes(patch), user_id=user_id)
|
|
116
117
|
|
|
117
118
|
async def send_changes_async(self):
|
|
118
|
-
|
|
119
|
+
user_id = None
|
|
120
|
+
if sly_env.is_multiuser_mode_enabled():
|
|
121
|
+
user_id = sly_env.user_from_multiuser_app()
|
|
122
|
+
await self.synchronize_changes(user_id=user_id)
|
|
119
123
|
|
|
120
124
|
def send_changes(self):
|
|
121
|
-
|
|
125
|
+
user_id = None
|
|
126
|
+
if sly_env.is_multiuser_mode_enabled():
|
|
127
|
+
user_id = sly_env.user_from_multiuser_app()
|
|
128
|
+
run_sync(self.synchronize_changes(user_id=user_id))
|
|
122
129
|
|
|
123
130
|
def raise_for_key(self, key: str):
|
|
124
131
|
if key in self:
|
|
@@ -139,7 +146,7 @@ class StateJson(_PatchableJson, metaclass=Singleton):
|
|
|
139
146
|
await StateJson._replace_global(dict(self))
|
|
140
147
|
|
|
141
148
|
@classmethod
|
|
142
|
-
async def from_request(cls, request: Request) -> StateJson:
|
|
149
|
+
async def from_request(cls, request: Request, local: bool = True) -> StateJson:
|
|
143
150
|
if "application/json" not in request.headers.get("Content-Type", ""):
|
|
144
151
|
return None
|
|
145
152
|
content = await request.json()
|
|
@@ -149,7 +156,8 @@ class StateJson(_PatchableJson, metaclass=Singleton):
|
|
|
149
156
|
# TODO: should we always replace STATE with {}?
|
|
150
157
|
d = content.get(Field.STATE, {})
|
|
151
158
|
await cls._replace_global(d)
|
|
152
|
-
|
|
159
|
+
|
|
160
|
+
return cls(d, __local__=local)
|
|
153
161
|
|
|
154
162
|
@classmethod
|
|
155
163
|
async def _replace_global(cls, d: dict):
|