supervisely 6.73.438__py3-none-any.whl → 6.73.513__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 (203) hide show
  1. supervisely/__init__.py +137 -1
  2. supervisely/_utils.py +81 -0
  3. supervisely/annotation/annotation.py +8 -2
  4. supervisely/annotation/json_geometries_map.py +14 -11
  5. supervisely/annotation/label.py +80 -3
  6. supervisely/api/annotation_api.py +14 -11
  7. supervisely/api/api.py +59 -38
  8. supervisely/api/app_api.py +11 -2
  9. supervisely/api/dataset_api.py +74 -12
  10. supervisely/api/entities_collection_api.py +10 -0
  11. supervisely/api/entity_annotation/figure_api.py +52 -4
  12. supervisely/api/entity_annotation/object_api.py +3 -3
  13. supervisely/api/entity_annotation/tag_api.py +63 -12
  14. supervisely/api/guides_api.py +210 -0
  15. supervisely/api/image_api.py +72 -1
  16. supervisely/api/labeling_job_api.py +83 -1
  17. supervisely/api/labeling_queue_api.py +33 -7
  18. supervisely/api/module_api.py +9 -0
  19. supervisely/api/project_api.py +71 -26
  20. supervisely/api/storage_api.py +3 -1
  21. supervisely/api/task_api.py +13 -2
  22. supervisely/api/team_api.py +4 -3
  23. supervisely/api/video/video_annotation_api.py +119 -3
  24. supervisely/api/video/video_api.py +65 -14
  25. supervisely/api/video/video_figure_api.py +24 -11
  26. supervisely/app/__init__.py +1 -1
  27. supervisely/app/content.py +23 -7
  28. supervisely/app/development/development.py +18 -2
  29. supervisely/app/fastapi/__init__.py +1 -0
  30. supervisely/app/fastapi/custom_static_files.py +1 -1
  31. supervisely/app/fastapi/multi_user.py +105 -0
  32. supervisely/app/fastapi/subapp.py +88 -42
  33. supervisely/app/fastapi/websocket.py +77 -9
  34. supervisely/app/singleton.py +21 -0
  35. supervisely/app/v1/app_service.py +18 -2
  36. supervisely/app/v1/constants.py +7 -1
  37. supervisely/app/widgets/__init__.py +6 -0
  38. supervisely/app/widgets/activity_feed/__init__.py +0 -0
  39. supervisely/app/widgets/activity_feed/activity_feed.py +239 -0
  40. supervisely/app/widgets/activity_feed/style.css +78 -0
  41. supervisely/app/widgets/activity_feed/template.html +22 -0
  42. supervisely/app/widgets/card/card.py +20 -0
  43. supervisely/app/widgets/classes_list_selector/classes_list_selector.py +121 -9
  44. supervisely/app/widgets/classes_list_selector/template.html +60 -93
  45. supervisely/app/widgets/classes_mapping/classes_mapping.py +13 -12
  46. supervisely/app/widgets/classes_table/classes_table.py +1 -0
  47. supervisely/app/widgets/deploy_model/deploy_model.py +56 -35
  48. supervisely/app/widgets/dialog/dialog.py +12 -0
  49. supervisely/app/widgets/dialog/template.html +2 -1
  50. supervisely/app/widgets/ecosystem_model_selector/ecosystem_model_selector.py +1 -1
  51. supervisely/app/widgets/experiment_selector/experiment_selector.py +8 -0
  52. supervisely/app/widgets/fast_table/fast_table.py +184 -60
  53. supervisely/app/widgets/fast_table/template.html +1 -1
  54. supervisely/app/widgets/heatmap/__init__.py +0 -0
  55. supervisely/app/widgets/heatmap/heatmap.py +564 -0
  56. supervisely/app/widgets/heatmap/script.js +533 -0
  57. supervisely/app/widgets/heatmap/style.css +233 -0
  58. supervisely/app/widgets/heatmap/template.html +21 -0
  59. supervisely/app/widgets/modal/__init__.py +0 -0
  60. supervisely/app/widgets/modal/modal.py +198 -0
  61. supervisely/app/widgets/modal/template.html +10 -0
  62. supervisely/app/widgets/object_class_view/object_class_view.py +3 -0
  63. supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
  64. supervisely/app/widgets/radio_tabs/template.html +1 -0
  65. supervisely/app/widgets/select/select.py +6 -3
  66. supervisely/app/widgets/select_class/__init__.py +0 -0
  67. supervisely/app/widgets/select_class/select_class.py +363 -0
  68. supervisely/app/widgets/select_class/template.html +50 -0
  69. supervisely/app/widgets/select_cuda/select_cuda.py +22 -0
  70. supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +65 -7
  71. supervisely/app/widgets/select_tag/__init__.py +0 -0
  72. supervisely/app/widgets/select_tag/select_tag.py +352 -0
  73. supervisely/app/widgets/select_tag/template.html +64 -0
  74. supervisely/app/widgets/select_team/select_team.py +37 -4
  75. supervisely/app/widgets/select_team/template.html +4 -5
  76. supervisely/app/widgets/select_user/__init__.py +0 -0
  77. supervisely/app/widgets/select_user/select_user.py +270 -0
  78. supervisely/app/widgets/select_user/template.html +13 -0
  79. supervisely/app/widgets/select_workspace/select_workspace.py +59 -10
  80. supervisely/app/widgets/select_workspace/template.html +9 -12
  81. supervisely/app/widgets/table/table.py +68 -13
  82. supervisely/app/widgets/tree_select/tree_select.py +2 -0
  83. supervisely/aug/aug.py +6 -2
  84. supervisely/convert/base_converter.py +1 -0
  85. supervisely/convert/converter.py +2 -2
  86. supervisely/convert/image/csv/csv_converter.py +24 -15
  87. supervisely/convert/image/image_converter.py +3 -1
  88. supervisely/convert/image/image_helper.py +48 -4
  89. supervisely/convert/image/label_studio/label_studio_converter.py +2 -0
  90. supervisely/convert/image/medical2d/medical2d_helper.py +2 -24
  91. supervisely/convert/image/multispectral/multispectral_converter.py +6 -0
  92. supervisely/convert/image/pascal_voc/pascal_voc_converter.py +8 -5
  93. supervisely/convert/image/pascal_voc/pascal_voc_helper.py +7 -0
  94. supervisely/convert/pointcloud/kitti_3d/kitti_3d_converter.py +33 -3
  95. supervisely/convert/pointcloud/kitti_3d/kitti_3d_helper.py +12 -5
  96. supervisely/convert/pointcloud/las/las_converter.py +13 -1
  97. supervisely/convert/pointcloud/las/las_helper.py +110 -11
  98. supervisely/convert/pointcloud/nuscenes_conv/nuscenes_converter.py +27 -16
  99. supervisely/convert/pointcloud/pointcloud_converter.py +91 -3
  100. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_converter.py +58 -22
  101. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_helper.py +21 -47
  102. supervisely/convert/video/__init__.py +1 -0
  103. supervisely/convert/video/multi_view/__init__.py +0 -0
  104. supervisely/convert/video/multi_view/multi_view.py +543 -0
  105. supervisely/convert/video/sly/sly_video_converter.py +359 -3
  106. supervisely/convert/video/video_converter.py +24 -4
  107. supervisely/convert/volume/dicom/dicom_converter.py +13 -5
  108. supervisely/convert/volume/dicom/dicom_helper.py +30 -18
  109. supervisely/geometry/constants.py +1 -0
  110. supervisely/geometry/geometry.py +4 -0
  111. supervisely/geometry/helpers.py +5 -1
  112. supervisely/geometry/oriented_bbox.py +676 -0
  113. supervisely/geometry/polyline_3d.py +110 -0
  114. supervisely/geometry/rectangle.py +2 -1
  115. supervisely/io/env.py +76 -1
  116. supervisely/io/fs.py +21 -0
  117. supervisely/nn/benchmark/base_evaluator.py +104 -11
  118. supervisely/nn/benchmark/instance_segmentation/evaluator.py +1 -8
  119. supervisely/nn/benchmark/object_detection/evaluator.py +20 -4
  120. supervisely/nn/benchmark/object_detection/vis_metrics/pr_curve.py +10 -5
  121. supervisely/nn/benchmark/semantic_segmentation/evaluator.py +34 -16
  122. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/confusion_matrix.py +1 -1
  123. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/frequently_confused.py +1 -1
  124. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/overview.py +1 -1
  125. supervisely/nn/benchmark/visualization/evaluation_result.py +66 -4
  126. supervisely/nn/inference/cache.py +43 -18
  127. supervisely/nn/inference/gui/serving_gui_template.py +5 -2
  128. supervisely/nn/inference/inference.py +916 -222
  129. supervisely/nn/inference/inference_request.py +55 -10
  130. supervisely/nn/inference/predict_app/gui/classes_selector.py +83 -12
  131. supervisely/nn/inference/predict_app/gui/gui.py +676 -488
  132. supervisely/nn/inference/predict_app/gui/input_selector.py +205 -26
  133. supervisely/nn/inference/predict_app/gui/model_selector.py +2 -4
  134. supervisely/nn/inference/predict_app/gui/output_selector.py +46 -6
  135. supervisely/nn/inference/predict_app/gui/settings_selector.py +756 -59
  136. supervisely/nn/inference/predict_app/gui/tags_selector.py +1 -1
  137. supervisely/nn/inference/predict_app/gui/utils.py +236 -119
  138. supervisely/nn/inference/predict_app/predict_app.py +2 -2
  139. supervisely/nn/inference/session.py +43 -35
  140. supervisely/nn/inference/tracking/bbox_tracking.py +118 -35
  141. supervisely/nn/inference/tracking/point_tracking.py +5 -1
  142. supervisely/nn/inference/tracking/tracker_interface.py +10 -1
  143. supervisely/nn/inference/uploader.py +139 -12
  144. supervisely/nn/live_training/__init__.py +7 -0
  145. supervisely/nn/live_training/api_server.py +111 -0
  146. supervisely/nn/live_training/artifacts_utils.py +243 -0
  147. supervisely/nn/live_training/checkpoint_utils.py +229 -0
  148. supervisely/nn/live_training/dynamic_sampler.py +44 -0
  149. supervisely/nn/live_training/helpers.py +14 -0
  150. supervisely/nn/live_training/incremental_dataset.py +146 -0
  151. supervisely/nn/live_training/live_training.py +497 -0
  152. supervisely/nn/live_training/loss_plateau_detector.py +111 -0
  153. supervisely/nn/live_training/request_queue.py +52 -0
  154. supervisely/nn/model/model_api.py +9 -0
  155. supervisely/nn/model/prediction.py +2 -1
  156. supervisely/nn/model/prediction_session.py +26 -14
  157. supervisely/nn/prediction_dto.py +19 -1
  158. supervisely/nn/tracker/base_tracker.py +11 -1
  159. supervisely/nn/tracker/botsort/botsort_config.yaml +0 -1
  160. supervisely/nn/tracker/botsort/tracker/mc_bot_sort.py +7 -4
  161. supervisely/nn/tracker/botsort_tracker.py +94 -65
  162. supervisely/nn/tracker/utils.py +4 -5
  163. supervisely/nn/tracker/visualize.py +93 -93
  164. supervisely/nn/training/gui/classes_selector.py +16 -1
  165. supervisely/nn/training/gui/train_val_splits_selector.py +52 -31
  166. supervisely/nn/training/train_app.py +46 -31
  167. supervisely/project/data_version.py +115 -51
  168. supervisely/project/download.py +1 -1
  169. supervisely/project/pointcloud_episode_project.py +37 -8
  170. supervisely/project/pointcloud_project.py +30 -2
  171. supervisely/project/project.py +14 -2
  172. supervisely/project/project_meta.py +27 -1
  173. supervisely/project/project_settings.py +32 -18
  174. supervisely/project/versioning/__init__.py +1 -0
  175. supervisely/project/versioning/common.py +20 -0
  176. supervisely/project/versioning/schema_fields.py +35 -0
  177. supervisely/project/versioning/video_schema.py +221 -0
  178. supervisely/project/versioning/volume_schema.py +87 -0
  179. supervisely/project/video_project.py +717 -15
  180. supervisely/project/volume_project.py +623 -5
  181. supervisely/template/experiment/experiment.html.jinja +4 -4
  182. supervisely/template/experiment/experiment_generator.py +14 -21
  183. supervisely/template/live_training/__init__.py +0 -0
  184. supervisely/template/live_training/header.html.jinja +96 -0
  185. supervisely/template/live_training/live_training.html.jinja +51 -0
  186. supervisely/template/live_training/live_training_generator.py +464 -0
  187. supervisely/template/live_training/sly-style.css +402 -0
  188. supervisely/template/live_training/template.html.jinja +18 -0
  189. supervisely/versions.json +28 -26
  190. supervisely/video/sampling.py +39 -20
  191. supervisely/video/video.py +41 -12
  192. supervisely/video_annotation/video_figure.py +38 -4
  193. supervisely/video_annotation/video_object.py +29 -4
  194. supervisely/volume/stl_converter.py +2 -0
  195. supervisely/worker_api/agent_rpc.py +24 -1
  196. supervisely/worker_api/rpc_servicer.py +31 -7
  197. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/METADATA +58 -40
  198. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/RECORD +203 -155
  199. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/WHEEL +1 -1
  200. supervisely_lib/__init__.py +6 -1
  201. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/entry_points.txt +0 -0
  202. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info/licenses}/LICENSE +0 -0
  203. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/top_level.txt +0 -0
@@ -1,11 +1,32 @@
1
+ import supervisely.io.env as sly_env
2
+
3
+
1
4
  class Singleton(type):
2
5
  _instances = {}
6
+ _nested_instances = {}
3
7
 
4
8
  def __call__(cls, *args, **kwargs):
5
9
  local = kwargs.pop("__local__", False)
6
10
  if local is False:
7
11
  if cls not in cls._instances:
8
12
  cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
13
+
14
+ if sly_env.is_multiuser_mode_enabled():
15
+ from supervisely.app.content import DataJson, StateJson
16
+ from copy import deepcopy
17
+
18
+ # Initialize nested instances dict once
19
+ nested_instances = cls._nested_instances.setdefault(cls, {})
20
+
21
+ user_id = sly_env.user_from_multiuser_app()
22
+ if user_id is not None and cls in (StateJson, DataJson):
23
+ if user_id not in nested_instances:
24
+ # Create new instance and copy data
25
+ instance = super(Singleton, cls).__call__(*args, **kwargs)
26
+ instance.update(deepcopy(dict(cls._instances[cls])))
27
+ nested_instances[user_id] = instance
28
+
29
+ return nested_instances[user_id]
9
30
  return cls._instances[cls]
10
31
  else:
11
32
  return super(Singleton, cls).__call__(*args, **kwargs)
@@ -1,3 +1,5 @@
1
+ # isort: skip_file
2
+
1
3
  import json
2
4
  import os
3
5
  import time
@@ -12,7 +14,8 @@ import queue
12
14
  import re
13
15
 
14
16
  from supervisely.worker_api.agent_api import AgentAPI
15
- from supervisely.worker_proto import worker_api_pb2 as api_proto
17
+
18
+ # from supervisely.worker_proto import worker_api_pb2 as api_proto # Import moved to methods where needed
16
19
  from supervisely.function_wrapper import function_wrapper
17
20
  from supervisely._utils import take_with_default
18
21
  from supervisely.sly_logger import logger as default_logger
@@ -30,7 +33,6 @@ from supervisely._utils import _remove_sensitive_information
30
33
  from supervisely.worker_api.agent_rpc import send_from_memory_generator
31
34
  from supervisely.io.fs_cache import FileCache
32
35
 
33
-
34
36
  # https://www.roguelynn.com/words/asyncio-we-did-it-wrong/
35
37
 
36
38
 
@@ -390,6 +392,13 @@ class AppService:
390
392
  )
391
393
 
392
394
  def publish_sync(self, initial_events=None):
395
+ try:
396
+ from supervisely.worker_proto import worker_api_pb2 as api_proto
397
+ except Exception as e:
398
+ from supervisely.app.v1.constants import PROTOBUF_REQUIRED_ERROR
399
+
400
+ raise ImportError(PROTOBUF_REQUIRED_ERROR) from e
401
+
393
402
  if initial_events is not None:
394
403
  for event_obj in initial_events:
395
404
  event_obj["api_token"] = os.environ[API_TOKEN]
@@ -507,6 +516,13 @@ class AppService:
507
516
  self._error = error
508
517
 
509
518
  def send_response(self, request_id, data):
519
+ try:
520
+ from supervisely.worker_proto import worker_api_pb2 as api_proto
521
+ except Exception as e:
522
+ from supervisely.app.v1.constants import PROTOBUF_REQUIRED_ERROR
523
+
524
+ raise ImportError(PROTOBUF_REQUIRED_ERROR) from e
525
+
510
526
  out_bytes = json.dumps(data).encode("utf-8")
511
527
  self.api.put_stream_with_data(
512
528
  "SendGeneralEventData",
@@ -7,4 +7,10 @@ SHARED_DATA = '/sessions'
7
7
 
8
8
  STOP_COMMAND = "stop"
9
9
 
10
- IMAGE_ANNOTATION_EVENTS = ["manual_selected_figure_changed"]
10
+ IMAGE_ANNOTATION_EVENTS = ["manual_selected_figure_changed"]
11
+
12
+ # Error message for missing or incompatible protobuf dependencies
13
+ PROTOBUF_REQUIRED_ERROR = (
14
+ "protobuf is required for agent/worker/app_v1 functionality. "
15
+ "Please install supervisely with agent extras: pip install 'supervisely[agent]'"
16
+ )
@@ -104,6 +104,8 @@ from supervisely.app.widgets.pie_chart.pie_chart import PieChart
104
104
  from supervisely.app.widgets.timeline.timeline import Timeline
105
105
  from supervisely.app.widgets.nodes_flow.nodes_flow import NodesFlow
106
106
  from supervisely.app.widgets.dialog.dialog import Dialog
107
+ from supervisely.app.widgets.modal.modal import Modal
108
+ from supervisely.app.widgets.activity_feed.activity_feed import ActivityFeed
107
109
  from supervisely.app.widgets.draggable.draggable import Draggable
108
110
  from supervisely.app.widgets.tooltip.tooltip import Tooltip
109
111
  from supervisely.app.widgets.image_annotation_preview.image_annotation_preview import (
@@ -129,6 +131,9 @@ from supervisely.app.widgets.classes_mapping_preview.classes_mapping_preview imp
129
131
  )
130
132
  from supervisely.app.widgets.classes_list_selector.classes_list_selector import ClassesListSelector
131
133
  from supervisely.app.widgets.classes_list_preview.classes_list_preview import ClassesListPreview
134
+ from supervisely.app.widgets.select_class.select_class import SelectClass
135
+ from supervisely.app.widgets.select_tag.select_tag import SelectTag
136
+ from supervisely.app.widgets.select_user.select_user import SelectUser
132
137
  from supervisely.app.widgets.tags_list_selector.tags_list_selector import TagsListSelector
133
138
  from supervisely.app.widgets.tags_list_preview.tags_list_preview import TagsListPreview
134
139
  from supervisely.app.widgets.members_list_selector.members_list_selector import MembersListSelector
@@ -161,3 +166,4 @@ from supervisely.app.widgets.dropdown_checkbox_selector.dropdown_checkbox_select
161
166
  from supervisely.app.widgets.ecosystem_model_selector.ecosystem_model_selector import (
162
167
  EcosystemModelSelector,
163
168
  )
169
+ from supervisely.app.widgets.heatmap.heatmap import Heatmap
File without changes
@@ -0,0 +1,239 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Dict, List, Optional
4
+
5
+ from supervisely.app import DataJson
6
+ from supervisely.app.widgets import Widget
7
+
8
+ try:
9
+ from typing import Literal
10
+ except ImportError:
11
+ from typing_extensions import Literal
12
+
13
+
14
+ class ActivityFeed(Widget):
15
+ """ActivityFeed is a widget that displays a vertical list of activity items with status indicators.
16
+ Similar to a timeline or activity log showing sequential events with their current status.
17
+
18
+ Each item can contain a custom widget as content and displays a status indicator (pending, in process, completed, failed).
19
+ Items are automatically numbered if no number is provided.
20
+
21
+ Read about it in `Developer Portal <https://developer.supervisely.com/app-development/widgets/layouts-and-containers/activity-feed>`_
22
+ (including screenshots and examples).
23
+
24
+ :param items: List of ActivityFeed.Item objects to display
25
+ :type items: Optional[List[ActivityFeed.Item]]
26
+ :param widget_id: An identifier of the widget.
27
+ :type widget_id: str, optional
28
+
29
+ :Usage example:
30
+ .. code-block:: python
31
+
32
+ from supervisely.app.widgets import ActivityFeed, Text
33
+
34
+ # Create items with custom content
35
+ item1 = ActivityFeed.Item(
36
+ content=Text("Processing dataset"),
37
+ status="completed"
38
+ )
39
+ item2 = ActivityFeed.Item(
40
+ content=Text("Training model"),
41
+ status="in_progress",
42
+ number=2
43
+ )
44
+ item3 = ActivityFeed.Item(
45
+ content=Text("Generating report"),
46
+ status="pending"
47
+ )
48
+
49
+ # Create activity feed
50
+ feed = ActivityFeed(items=[item1, item2, item3])
51
+
52
+ # Add item during runtime
53
+ new_item = ActivityFeed.Item(
54
+ content=Text("Deploy model"),
55
+ status="pending"
56
+ )
57
+ feed.add_item(new_item)
58
+
59
+ # Update status by item number
60
+ feed.set_status(2, "completed")
61
+
62
+ # Get item status
63
+ status = feed.get_status(2)
64
+ """
65
+
66
+ class Item:
67
+ """Represents a single item in the ActivityFeed.
68
+
69
+ :param content: Widget to display as the item content
70
+ :type content: Widget
71
+ :param status: Status of the item (pending, in_progress, completed, failed)
72
+ :type status: Literal["pending", "in_progress", "completed", "failed"]
73
+ :param number: Position number in the feed (auto-assigned if not provided)
74
+ :type number: Optional[int]
75
+ """
76
+
77
+ def __init__(
78
+ self,
79
+ content: Widget,
80
+ status: Literal["pending", "in_progress", "completed", "failed"] = "pending",
81
+ number: Optional[int] = None,
82
+ ) -> ActivityFeed.Item:
83
+ self.content = content
84
+ self.status = status
85
+ self.number = number
86
+ self._validate_status()
87
+
88
+ def _validate_status(self):
89
+ valid_statuses = ["pending", "in_progress", "completed", "failed"]
90
+ if self.status not in valid_statuses:
91
+ raise ValueError(
92
+ f"Invalid status '{self.status}'. Must be one of: {', '.join(valid_statuses)}"
93
+ )
94
+
95
+ def to_json(self):
96
+ return {
97
+ "number": self.number,
98
+ "status": self.status,
99
+ }
100
+
101
+ def __init__(
102
+ self,
103
+ items: Optional[List[ActivityFeed.Item]] = None,
104
+ widget_id: Optional[str] = None,
105
+ ):
106
+ self._items = items if items is not None else []
107
+ self._auto_assign_numbers()
108
+ super().__init__(widget_id=widget_id, file_path=__file__)
109
+
110
+ def _auto_assign_numbers(self):
111
+ """Automatically assign numbers to items that don't have them."""
112
+ next_number = 1
113
+ for item in self._items:
114
+ if item.number is None:
115
+ item.number = next_number
116
+ next_number = max(next_number, item.number) + 1
117
+
118
+ def get_json_data(self) -> Dict:
119
+ """Returns dictionary with widget data.
120
+
121
+ :return: Dictionary with items data
122
+ :rtype: Dict
123
+ """
124
+ return {
125
+ "items": [item.to_json() for item in self._items],
126
+ }
127
+
128
+ def get_json_state(self) -> Dict:
129
+ """Returns dictionary with widget state (empty for this widget).
130
+
131
+ :return: Empty dictionary
132
+ :rtype: Dict
133
+ """
134
+ return {}
135
+
136
+ def add_item(
137
+ self,
138
+ item: Optional[ActivityFeed.Item] = None,
139
+ content: Optional[Widget] = None,
140
+ status: Literal["pending", "in_progress", "completed", "failed"] = "pending",
141
+ number: Optional[int] = None,
142
+ ) -> None:
143
+ """Add a new item to the activity feed.
144
+
145
+ You can either pass an ActivityFeed.Item object or provide content and status separately.
146
+
147
+ :param item: ActivityFeed.Item to add
148
+ :type item: Optional[ActivityFeed.Item]
149
+ :param content: Widget content (used if item is not provided)
150
+ :type content: Optional[Widget]
151
+ :param status: Status of the item (used if item is not provided)
152
+ :type status: Literal["pending", "in_progress", "completed", "failed"]
153
+ :param number: Position number (auto-assigned if not provided)
154
+ :type number: Optional[int]
155
+ """
156
+ if item is None:
157
+ if content is None:
158
+ raise ValueError("Either 'item' or 'content' must be provided")
159
+ item = ActivityFeed.Item(content=content, status=status, number=number)
160
+
161
+ if item.number is None:
162
+ # Auto-assign number
163
+ if self._items:
164
+ item.number = max(i.number for i in self._items) + 1
165
+ else:
166
+ item.number = 1
167
+
168
+ self._items.append(item)
169
+ self.update_data()
170
+ DataJson().send_changes()
171
+
172
+ def remove_item(self, number: int) -> None:
173
+ """Remove an item from the activity feed by its number.
174
+
175
+ :param number: Number of the item to remove. Starts from 1.
176
+ :type number: int
177
+ """
178
+ self._items = [item for item in self._items if item.number != number]
179
+ self.update_data()
180
+ DataJson().send_changes()
181
+
182
+ def set_status(
183
+ self,
184
+ number: int,
185
+ status: Literal["pending", "in_progress", "completed", "failed"],
186
+ ) -> None:
187
+ """Update the status of an item by its number.
188
+
189
+ :param number: Number of the item to update. Starts from 1.
190
+ :type number: int
191
+ :param status: New status for the item
192
+ :type status: Literal["pending", "in_progress", "completed", "failed"]
193
+ """
194
+ for i, item in enumerate(self._items):
195
+ if item.number == number:
196
+ item.status = status
197
+ item._validate_status()
198
+ DataJson()[self.widget_id]["items"][i]["status"] = status
199
+ DataJson().send_changes()
200
+ return
201
+ raise ValueError(f"Item with number {number} not found")
202
+
203
+ def get_status(self, number: int) -> str:
204
+ """Get the status of an item by its number.
205
+
206
+ :param number: Number of the item. Starts from 1.
207
+ :type number: int
208
+ :return: Status of the item
209
+ :rtype: str
210
+ """
211
+ for item in self._items:
212
+ if item.number == number:
213
+ return item.status
214
+ raise ValueError(f"Item with number {number} not found")
215
+
216
+ def get_items(self) -> List[ActivityFeed.Item]:
217
+ """Get all items in the activity feed.
218
+
219
+ :return: List of all items
220
+ :rtype: List[ActivityFeed.Item]
221
+ """
222
+ return self._items
223
+
224
+ def clear(self) -> None:
225
+ """Remove all items from the activity feed."""
226
+ self._items = []
227
+ self.update_data()
228
+ DataJson().send_changes()
229
+
230
+ def set_items(self, items: List[ActivityFeed.Item]) -> None:
231
+ """Replace all items in the activity feed.
232
+
233
+ :param items: New list of items
234
+ :type items: List[ActivityFeed.Item]
235
+ """
236
+ self._items = items
237
+ self._auto_assign_numbers()
238
+ self.update_data()
239
+ DataJson().send_changes()
@@ -0,0 +1,78 @@
1
+ .sly-activity-feed {
2
+ padding: 10px;
3
+ }
4
+
5
+ .sly-activity-feed-item {
6
+ display: flex;
7
+ position: relative;
8
+ padding-bottom: 20px;
9
+ }
10
+
11
+ .sly-activity-feed-item:last-child {
12
+ padding-bottom: 0;
13
+ }
14
+
15
+ .sly-activity-feed-item:not(:last-child) .sly-activity-feed-line {
16
+ display: block;
17
+ }
18
+
19
+ .sly-activity-feed-marker {
20
+ flex-shrink: 0;
21
+ width: 40px;
22
+ display: flex;
23
+ flex-direction: column;
24
+ align-items: center;
25
+ position: relative;
26
+ }
27
+
28
+ .sly-activity-feed-circle {
29
+ width: 16px;
30
+ height: 16px;
31
+ border-radius: 50%;
32
+ display: flex;
33
+ align-items: center;
34
+ justify-content: center;
35
+ flex-shrink: 0;
36
+ font-size: 10px;
37
+ color: white;
38
+ z-index: 1;
39
+ }
40
+
41
+ .sly-activity-feed-circle.completed {
42
+ background-color: #67C23A;
43
+ }
44
+
45
+ .sly-activity-feed-circle.failed {
46
+ background-color: #F56C6C;
47
+ }
48
+
49
+ .sly-activity-feed-circle.in_progress {
50
+ background-color: #409EFF;
51
+ }
52
+
53
+ .sly-activity-feed-circle.pending {
54
+ background-color: #909399;
55
+ }
56
+
57
+ .sly-activity-feed-line {
58
+ position: absolute;
59
+ top: 16px;
60
+ left: 50%;
61
+ transform: translateX(-50%);
62
+ width: 2px;
63
+ height: calc(100% + 4px);
64
+ background-color: #E4E7ED;
65
+ display: none;
66
+ }
67
+
68
+ .sly-activity-feed-content {
69
+ flex: 1;
70
+ padding-left: 10px;
71
+ padding-top: 0px;
72
+ }
73
+
74
+ .sly-activity-feed-number {
75
+ font-size: 11px;
76
+ color: #909399;
77
+ margin-top: 4px;
78
+ }
@@ -0,0 +1,22 @@
1
+ <link rel="stylesheet" href="./sly/css/app/widgets/activity_feed/style.css" />
2
+
3
+ <div class="sly-activity-feed">
4
+ {% for item in widget._items %}
5
+ <div class="sly-activity-feed-item" v-if="data.{{{widget.widget_id}}}.items[{{{loop.index0}}}]">
6
+ <div class="sly-activity-feed-marker">
7
+ <div class="sly-activity-feed-circle" :class="data.{{{widget.widget_id}}}.items[{{{loop.index0}}}].status">
8
+ <i v-if="data.{{{widget.widget_id}}}.items[{{{loop.index0}}}].status === 'completed'" class="el-icon-check"></i>
9
+ <i v-else-if="data.{{{widget.widget_id}}}.items[{{{loop.index0}}}].status === 'failed'"
10
+ class="el-icon-close"></i>
11
+ <i v-else-if="data.{{{widget.widget_id}}}.items[{{{loop.index0}}}].status === 'in_progress'"
12
+ class="el-icon-loading"></i>
13
+ <i v-else class="el-icon-time"></i>
14
+ </div>
15
+ <div class="sly-activity-feed-line"></div>
16
+ </div>
17
+ <div class="sly-activity-feed-content">
18
+ {{{item.content}}}
19
+ </div>
20
+ </div>
21
+ {% endfor %}
22
+ </div>
@@ -157,3 +157,23 @@ class Card(Widget):
157
157
  :rtype: bool
158
158
  """
159
159
  return self._disabled["disabled"]
160
+
161
+ @property
162
+ def description(self) -> Optional[str]:
163
+ """Description of the card.
164
+
165
+ :return: Description of the card.
166
+ :rtype: Optional[str]
167
+ """
168
+ return self._description
169
+
170
+ @description.setter
171
+ def description(self, value: str) -> None:
172
+ """Sets the description of the card.
173
+
174
+ :param value: Description of the card.
175
+ :type value: str
176
+ """
177
+ self._description = value
178
+ StateJson()[self.widget_id]["description"] = self._description
179
+ StateJson().send_changes()