supervisely 6.73.287__py3-none-any.whl → 6.73.289__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.
@@ -543,7 +543,7 @@ class Annotation:
543
543
  image_id=take_with_default(image_id, self.image_id),
544
544
  )
545
545
 
546
- def _add_labels_impl(self, dest, labels):
546
+ def _add_labels_impl(self, dest: List, labels: List[Label]):
547
547
  """
548
548
  The function _add_labels_impl extend list of the labels of the current Annotation object
549
549
  :param dest: destination list of the Label class objects
@@ -554,7 +554,15 @@ class Annotation:
554
554
  if self.img_size.count(None) == 0:
555
555
  # image has resolution in DB
556
556
  canvas_rect = Rectangle.from_size(self.img_size)
557
- dest.extend(label.crop(canvas_rect))
557
+ try:
558
+ dest.extend(label.crop(canvas_rect))
559
+ except Exception:
560
+ logger.error(
561
+ f"Cannot crop label of '{label.obj_class.name}' class "
562
+ "when extend list of the labels of the current Annotation object",
563
+ exc_info=True,
564
+ )
565
+ raise
558
566
  else:
559
567
  # image was uploaded by link and does not have resolution in DB
560
568
  # add label without normalization and validation
@@ -1403,37 +1403,33 @@ class AnnotationApi(ModuleApi):
1403
1403
  progress_cb(len(response.content))
1404
1404
 
1405
1405
  result = response.json()
1406
- # Convert annotation to pixel coordinate system
1407
- result[ApiField.ANNOTATION] = Annotation._to_pixel_coordinate_system_json(
1408
- result[ApiField.ANNOTATION]
1406
+ # Convert annotation to pixel coordinate system
1407
+ result[ApiField.ANNOTATION] = Annotation._to_pixel_coordinate_system_json(
1408
+ result[ApiField.ANNOTATION]
1409
+ )
1410
+ # check if there are any AlphaMask geometries in the batch
1411
+ additonal_geometries = defaultdict(int)
1412
+ labels = result[ApiField.ANNOTATION][AnnotationJsonFields.LABELS]
1413
+ for idx, label in enumerate(labels):
1414
+ if label[LabelJsonFields.GEOMETRY_TYPE] == AlphaMask.geometry_name():
1415
+ figure_id = label[LabelJsonFields.ID]
1416
+ additonal_geometries[figure_id] = idx
1417
+
1418
+ # if so, download them separately and update the annotation
1419
+ if len(additonal_geometries) > 0:
1420
+ figure_ids = list(additonal_geometries.keys())
1421
+ figures = await self._api.image.figure.download_geometries_batch_async(
1422
+ figure_ids,
1423
+ (progress_cb if progress_cb is not None and progress_cb_type == "size" else None),
1424
+ semaphore=semaphore,
1409
1425
  )
1410
- # check if there are any AlphaMask geometries in the batch
1411
- additonal_geometries = defaultdict(int)
1412
- labels = result[ApiField.ANNOTATION][AnnotationJsonFields.LABELS]
1413
- for idx, label in enumerate(labels):
1414
- if label[LabelJsonFields.GEOMETRY_TYPE] == AlphaMask.geometry_name():
1415
- figure_id = label[LabelJsonFields.ID]
1416
- additonal_geometries[figure_id] = idx
1417
-
1418
- # if so, download them separately and update the annotation
1419
- if len(additonal_geometries) > 0:
1420
- figure_ids = list(additonal_geometries.keys())
1421
- figures = await self._api.image.figure.download_geometries_batch_async(
1422
- figure_ids,
1423
- (
1424
- progress_cb
1425
- if progress_cb is not None and progress_cb_type == "size"
1426
- else None
1427
- ),
1428
- semaphore=semaphore,
1429
- )
1430
- for figure_id, geometry in zip(figure_ids, figures):
1431
- label_idx = additonal_geometries[figure_id]
1432
- labels[label_idx].update({BITMAP: geometry})
1433
- ann_info = self._convert_json_info(result)
1434
- if progress_cb is not None and progress_cb_type == "number":
1435
- progress_cb(1)
1436
- return ann_info
1426
+ for figure_id, geometry in zip(figure_ids, figures):
1427
+ label_idx = additonal_geometries[figure_id]
1428
+ labels[label_idx].update({BITMAP: geometry})
1429
+ ann_info = self._convert_json_info(result)
1430
+ if progress_cb is not None and progress_cb_type == "number":
1431
+ progress_cb(1)
1432
+ return ann_info
1437
1433
 
1438
1434
  async def download_batch_async(
1439
1435
  self,
supervisely/api/api.py CHANGED
@@ -66,10 +66,10 @@ import supervisely.io.env as sly_env
66
66
  from supervisely._utils import camel_to_snake, is_community, is_development
67
67
  from supervisely.api.module_api import ApiField
68
68
  from supervisely.io.network_exceptions import (
69
+ RetryableRequestException,
69
70
  process_requests_exception,
70
71
  process_requests_exception_async,
71
72
  process_unhandled_request,
72
- RetryableRequestException,
73
73
  )
74
74
  from supervisely.project.project_meta import ProjectMeta
75
75
  from supervisely.sly_logger import logger
@@ -380,6 +380,7 @@ class Api:
380
380
  self.async_httpx_client: httpx.AsyncClient = None
381
381
  self.httpx_client: httpx.Client = None
382
382
  self._semaphore = None
383
+ self._instance_version = None
383
384
 
384
385
  @classmethod
385
386
  def normalize_server_address(cls, server_address: str) -> str:
@@ -515,11 +516,14 @@ class Api:
515
516
  # '6.9.13'
516
517
  """
517
518
  try:
518
- version = self.post("instance.version", {}).json().get(ApiField.VERSION)
519
+ if self._instance_version is None:
520
+ self._instance_version = (
521
+ self.post("instance.version", {}).json().get(ApiField.VERSION)
522
+ )
519
523
  except Exception as e:
520
524
  logger.warning(f"Failed to get instance version from server: {e}")
521
- version = "unknown"
522
- return version
525
+ self._instance_version = "unknown"
526
+ return self._instance_version
523
527
 
524
528
  def is_version_supported(self, version: Optional[str] = None) -> Union[bool, None]:
525
529
  """Check if the given version is lower or equal to the current Supervisely instance version.
@@ -642,13 +642,13 @@ class FigureApi(RemoveableBulkModuleApi):
642
642
  response = await self._api.post_async(
643
643
  "figures.bulk.download.geometry", {ApiField.IDS: batch_ids}
644
644
  )
645
- decoder = MultipartDecoder.from_response(response)
646
- for part in decoder.parts:
647
- content_utf8 = part.headers[b"Content-Disposition"].decode("utf-8")
648
- # Find name="1245" preceded by a whitespace, semicolon or beginning of line.
649
- # The regex has 2 capture group: one for the prefix and one for the actual name value.
650
- figure_id = int(re.findall(r'(^|[\s;])name="(\d*)"', content_utf8)[0][1])
651
- yield figure_id, part.content
645
+ decoder = MultipartDecoder.from_response(response)
646
+ for part in decoder.parts:
647
+ content_utf8 = part.headers[b"Content-Disposition"].decode("utf-8")
648
+ # Find name="1245" preceded by a whitespace, semicolon or beginning of line.
649
+ # The regex has 2 capture group: one for the prefix and one for the actual name value.
650
+ figure_id = int(re.findall(r'(^|[\s;])name="(\d*)"', content_utf8)[0][1])
651
+ yield figure_id, part.content
652
652
 
653
653
  async def download_geometries_batch_async(
654
654
  self,
@@ -4456,26 +4456,26 @@ class ImageApi(RemoveableBulkModuleApi):
4456
4456
  json=json_body,
4457
4457
  headers=headers,
4458
4458
  )
4459
- decoder = MultipartDecoder.from_response(response)
4460
- for part in decoder.parts:
4461
- content_utf8 = part.headers[b"Content-Disposition"].decode("utf-8")
4462
- # Find name="1245" preceded by a whitespace, semicolon or beginning of line.
4463
- # The regex has 2 capture group: one for the prefix and one for the actual name value.
4464
- img_id = int(re.findall(r'(^|[\s;])name="(\d*)"', content_utf8)[0][1])
4465
- if check_hash:
4466
- hhash = part.headers.get("x-content-checksum-sha256", None)
4467
- if hhash is not None:
4468
- downloaded_bytes_hash = get_bytes_hash(part)
4469
- if hhash != downloaded_bytes_hash:
4470
- raise RuntimeError(
4471
- f"Downloaded hash of image with ID:{img_id} does not match the expected hash: {downloaded_bytes_hash} != {hhash}"
4472
- )
4473
- if progress_cb is not None and progress_cb_type == "number":
4474
- progress_cb(1)
4475
- elif progress_cb is not None and progress_cb_type == "size":
4476
- progress_cb(len(part.content))
4459
+ decoder = MultipartDecoder.from_response(response)
4460
+ for part in decoder.parts:
4461
+ content_utf8 = part.headers[b"Content-Disposition"].decode("utf-8")
4462
+ # Find name="1245" preceded by a whitespace, semicolon or beginning of line.
4463
+ # The regex has 2 capture group: one for the prefix and one for the actual name value.
4464
+ img_id = int(re.findall(r'(^|[\s;])name="(\d*)"', content_utf8)[0][1])
4465
+ if check_hash:
4466
+ hhash = part.headers.get("x-content-checksum-sha256", None)
4467
+ if hhash is not None:
4468
+ downloaded_bytes_hash = get_bytes_hash(part)
4469
+ if hhash != downloaded_bytes_hash:
4470
+ raise RuntimeError(
4471
+ f"Downloaded hash of image with ID:{img_id} does not match the expected hash: {downloaded_bytes_hash} != {hhash}"
4472
+ )
4473
+ if progress_cb is not None and progress_cb_type == "number":
4474
+ progress_cb(1)
4475
+ elif progress_cb is not None and progress_cb_type == "size":
4476
+ progress_cb(len(part.content))
4477
4477
 
4478
- yield img_id, part.content
4478
+ yield img_id, part.content
4479
4479
 
4480
4480
  async def get_list_generator_async(
4481
4481
  self,
@@ -4,17 +4,13 @@ import asyncio
4
4
  import re
5
5
  from abc import ABC, abstractmethod
6
6
  from datetime import datetime
7
- from typing import Any, Callable, Dict, List, Literal, Optional, Union
7
+ from typing import Any, Callable, List, Literal, Optional
8
8
  from uuid import uuid4
9
9
 
10
10
  from fastapi.responses import HTMLResponse
11
11
  from pydantic import BaseModel
12
12
 
13
- from supervisely._utils import is_production
14
- from supervisely.api.api import Api
15
13
  from supervisely.app.widgets import Widget
16
- from supervisely.io.env import task_id
17
- from supervisely.sly_logger import logger
18
14
 
19
15
 
20
16
  class DebouncedEventHandler:
@@ -37,7 +33,6 @@ class DebouncedEventHandler:
37
33
 
38
34
  class SelectedIds(BaseModel):
39
35
  selected_ids: List[int]
40
- plot_id: Optional[Union[str, int]] = None
41
36
 
42
37
 
43
38
  class Bokeh(Widget):
@@ -77,14 +72,25 @@ class Bokeh(Widget):
77
72
 
78
73
  from supervisely.app.widgets import Bokeh, IFrame
79
74
 
80
- plot = Bokeh.Circle(
81
- x_coordinates=[1, 2, 3, 4, 5],
82
- y_coordinates=[1, 2, 3, 4, 5],
83
- radii=10,
84
- colors="red",
85
- legend_label="Circle plot",
75
+ data = {
76
+ "x": [1, 2, 3, 4, 5],
77
+ "y": [1, 2, 3, 4, 5],
78
+ "radius": [10, 20, 30, 40, 50],
79
+ "colors": ["red", "green", "blue", "yellow", "purple"],
80
+ "ids": [1, 2, 3, 4, 5],
81
+ "names": ["kiwi", "kiwi", "lemon", "lemon", "lemon"],
82
+ }
83
+
84
+ plot_lemon = Bokeh.Circle(name="lemon")
85
+ plot_kiwi = Bokeh.Circle(name="kiwi")
86
+ bokeh = Bokeh(
87
+ x_axis_visible=True,
88
+ y_axis_visible=True,
89
+ grid_visible=True,
90
+ show_legend=True,
86
91
  )
87
- bokeh = Bokeh(plots=[plot], width=1000, height=600)
92
+ bokeh.add_data(**data)
93
+ bokeh.add_plots([plot_lemon, plot_kiwi])
88
94
 
89
95
  # To allow the widget to be interacted with, you need to add it to the IFrame widget.
90
96
  iframe = IFrame()
@@ -96,126 +102,72 @@ class Bokeh(Widget):
96
102
  HTML_ROUTE = "bokeh.html"
97
103
 
98
104
  class Plot(ABC):
99
- @abstractmethod
100
- def add(self, plot) -> None:
101
- pass
105
+ def __init__(self, name: Optional[str] = None, **kwargs):
106
+
107
+ self._name = name or str(uuid4())
108
+ self.kwargs = kwargs
102
109
 
103
110
  @abstractmethod
104
- def register(self, route_path: str) -> None:
111
+ def add(self, plot, source) -> None:
105
112
  pass
106
113
 
114
+ @property
115
+ def name(self) -> str:
116
+ return self._name
117
+
107
118
  class Circle(Plot):
108
- def __init__(
109
- self,
110
- x_coordinates: List[Union[int, float]],
111
- y_coordinates: List[Union[int, float]],
112
- radii: Optional[Union[Union[int, float], List[Union[int, float]]]] = None,
113
- colors: Optional[Union[str, List[str]]] = None,
114
- data: Optional[List[Any]] = None,
115
- dynamic_selection: bool = False,
116
- fill_alpha: float = 0.5,
117
- line_color: Optional[str] = None,
118
- legend_label: Optional[str] = None,
119
- plot_id: Optional[Union[str, int]] = None,
120
- ):
121
- if not colors:
122
- colors = Bokeh._generate_colors(x_coordinates, y_coordinates)
123
- elif isinstance(colors, str):
124
- colors = [colors] * len(x_coordinates)
125
-
126
- if not radii:
127
- radii = Bokeh._generate_radii(x_coordinates, y_coordinates)
128
- elif isinstance(radii, (int, float)):
129
- radii = [radii] * len(x_coordinates)
130
-
131
- if not len(x_coordinates) == len(y_coordinates) == len(radii) == len(colors):
132
- raise ValueError(
133
- "x_coordinates, y_coordinates, radii, and colors must have the same length"
134
- )
135
-
136
- if data is not None and len(data) != len(x_coordinates):
137
- raise ValueError("data must have the same length as x_coordinates")
138
-
139
- if data is None:
140
- data = list(range(len(x_coordinates)))
141
-
142
- self._x_coordinates = x_coordinates
143
- self._y_coordinates = y_coordinates
144
- self._radii = radii
145
- self._colors = colors
146
- self._data = data
147
- self._source = None
148
- self._dynamic_selection = dynamic_selection
149
- self._fill_alpha = fill_alpha
150
- self._line_color = line_color
151
- self._plot_id = plot_id or uuid4().hex
152
- self._legend_label = legend_label or str(self._plot_id)
153
-
154
- def add(self, plot) -> None:
119
+ def add(self, plot, source) -> None:
155
120
  from bokeh.models import ( # pylint: disable=import-error
156
- ColumnDataSource,
157
- LassoSelectTool,
158
- )
159
-
160
- data = dict(
161
- x=self._x_coordinates,
162
- y=self._y_coordinates,
163
- radius=self._radii,
164
- colors=self._colors,
165
- ids=self._data,
121
+ CDSView,
122
+ GroupFilter,
166
123
  )
167
- self._source = ColumnDataSource(data=data)
168
124
 
169
- renderer = plot.circle(
125
+ filters = [GroupFilter(column_name="names", group=self.name, name=self.name)]
126
+ view = CDSView(source=source, filters=filters)
127
+ return plot.circle(
170
128
  "x",
171
129
  "y",
172
130
  radius="radius",
173
131
  fill_color="colors",
174
- fill_alpha=self._fill_alpha,
175
- line_color=self._line_color,
176
- source=self._source,
177
- legend_label=self._legend_label,
132
+ fill_alpha=0.5,
133
+ source=source,
134
+ line_color=None,
135
+ view=view,
178
136
  )
179
- if not self._dynamic_selection:
180
- for tool in plot.tools:
181
- if isinstance(tool, (LassoSelectTool)):
182
- tool.continuous = False
183
-
184
- return renderer
185
-
186
- def register(self, route_path: str) -> None:
187
- from bokeh.models import CustomJS # pylint: disable=import-error
188
-
189
- if not hasattr(self, "_source"):
190
- raise ValueError("Plot must be added to a Bokeh plot before registering")
191
-
192
- if is_production():
193
- api = Api()
194
- task_info = api.task.get_info_by_id(task_id())
195
- if task_info is not None:
196
- route_path = f"/net/{task_info['meta']['sessionToken']}{route_path}"
197
- callback = CustomJS(
198
- args=dict(source=self._source),
199
- code="""
200
- var indices = source.selected.indices;
201
- var selected_ids = [];
202
- for (var i = 0; i < indices.length; i++) {{
203
- selected_ids.push(source.data['ids'][indices[i]]);
204
- }}
205
- var xhr = new XMLHttpRequest();
206
- xhr.open("POST", "{route_path}", true);
207
- xhr.setRequestHeader("Content-Type", "application/json");
208
- xhr.send(JSON.stringify({{selected_ids: selected_ids, plot_id: '{plot_id}'}}));
209
- """.format(
210
- route_path=route_path,
211
- plot_id=self._plot_id,
212
- ),
137
+
138
+ class Scatter(Plot):
139
+ def add(self, plot, source) -> None:
140
+ from bokeh.models import ( # pylint: disable=import-error
141
+ CDSView,
142
+ GroupFilter,
143
+ )
144
+
145
+ filters = [GroupFilter(column_name="names", group=self.name, name=self.name)]
146
+ view = CDSView(source=source, filters=filters)
147
+ return plot.scatter(
148
+ "x",
149
+ "y",
150
+ size="radius",
151
+ color="colors",
152
+ fill_alpha=0.5,
153
+ source=source,
154
+ view=view,
155
+ )
156
+
157
+ class Line(Plot):
158
+ def add(self, plot, source) -> None:
159
+ from bokeh.models import ( # pylint: disable=import-error
160
+ CDSView,
161
+ GroupFilter,
213
162
  )
214
- self._source.selected.js_on_change("indices", callback)
163
+
164
+ filters = [GroupFilter(column_name="names", group=self.name, name=self.name)]
165
+ view = CDSView(source=source, filters=filters)
166
+ return plot.line("x", "y", source=source, view=view, line_width=2)
215
167
 
216
168
  def __init__(
217
169
  self,
218
- plots: List[Plot],
170
+ plots: List[Plot] = None,
219
171
  width: int = 1000,
220
172
  height: int = 600,
221
173
  tools: List[str] = [
@@ -223,7 +175,7 @@ class Bokeh(Widget):
223
175
  "wheel_zoom",
224
176
  "box_zoom",
225
177
  "reset",
226
- "save",
178
+ # "save",
227
179
  "poly_select",
228
180
  "tap",
229
181
  "lasso_select",
@@ -244,8 +196,11 @@ class Bokeh(Widget):
244
196
  if bokeh.__version__ != "3.1.1":
245
197
  raise RuntimeError(f"Bokeh version {bokeh.__version__} is not supported. Use 3.1.1")
246
198
 
199
+ self._source_data = {"x": [], "y": [], "radius": [], "colors": [], "ids": [], "names": []}
200
+ self._source = None
247
201
  self.widget_id = widget_id
248
- self._plots = plots
202
+ self._plots = plots or []
203
+ self._view = None
249
204
 
250
205
  self._width = width
251
206
  self._height = height
@@ -259,56 +214,14 @@ class Bokeh(Widget):
259
214
  self._legend_click_policy = legend_click_policy
260
215
 
261
216
  super().__init__(widget_id=widget_id, file_path=__file__)
262
- self._load_chart()
217
+
218
+ self._update()
263
219
 
264
220
  server = self._sly_app.get_server()
265
221
 
266
222
  @server.get(self.html_route)
267
223
  def _html_response() -> None:
268
- return HTMLResponse(content=self.get_html())
269
-
270
- # TODO: support for offline mode
271
- # JinjaWidgets().context.pop(self.widget_id, None) # remove the widget from index.html
272
-
273
- def _load_chart(self, **kwargs):
274
- from bokeh.models import Legend # pylint: disable=import-error
275
- from bokeh.plotting import figure # pylint: disable=import-error
276
-
277
- self._width = kwargs.get("width", self._width)
278
- self._height = kwargs.get("height", self._height)
279
- self._tools = kwargs.get("tools", self._tools)
280
- self._toolbar_location = kwargs.get("toolbar_location", self._toolbar_location)
281
- self._show_legend = kwargs.get("show_legend", self._show_legend)
282
- self._legend_location = kwargs.get("legend_location", self._legend_location)
283
- self._legend_click_policy = kwargs.get("legend_click_policy", self._legend_click_policy)
284
- self._x_axis_visible = kwargs.get("x_axis_visible", self._x_axis_visible)
285
- self._y_axis_visible = kwargs.get("y_axis_visible", self._y_axis_visible)
286
- self._grid_visible = kwargs.get("grid_visible", self._grid_visible)
287
-
288
- self._plot = figure(
289
- width=self._width,
290
- height=self._height,
291
- tools=self._tools,
292
- toolbar_location=self._toolbar_location,
293
- )
294
-
295
- if self._show_legend:
296
- self._plot.add_layout(
297
- Legend(click_policy=self._legend_click_policy),
298
- self._legend_location,
299
- )
300
-
301
- self._plot.xaxis.visible = self._x_axis_visible
302
- self._plot.yaxis.visible = self._y_axis_visible
303
- self._plot.grid.visible = self._grid_visible
304
-
305
- self._renderers = []
306
- self._process_plots(self._plots)
307
- self._update_html()
308
-
309
- @property
310
- def route_path(self) -> str:
311
- return self.get_route_path(Bokeh.Routes.VALUE_CHANGED)
224
+ return HTMLResponse(content=self._get_html())
312
225
 
313
226
  @property
314
227
  def html_route(self) -> str:
@@ -318,48 +231,75 @@ class Bokeh(Widget):
318
231
  def html_route_with_timestamp(self) -> str:
319
232
  return f".{self.html_route}?t={datetime.now().timestamp()}"
320
233
 
321
- def add_plots(self, plots: List[Plot]) -> None:
322
- self._plots.extend(plots)
323
- self._process_plots(plots)
324
- self._update_html()
234
+ def get_json_data(self):
235
+ return {}
325
236
 
326
- def clear(self) -> None:
327
- self._plots = []
328
- self._renderers = []
329
- self._plot.renderers = []
330
- self._update_html()
237
+ def get_json_state(self):
238
+ return {}
331
239
 
332
- def remove_plot(self, idx: int) -> None:
333
- renderer = self._renderers.pop(idx)
334
- self._plot.renderers.remove(renderer)
335
- self._update_html()
240
+ def _add_callbacks(self):
241
+ from bokeh.models import CustomJS # pylint: disable=import-error
242
+
243
+ route_path = self.get_route_path(Bokeh.Routes.VALUE_CHANGED)
244
+ callback = CustomJS(
245
+ args=dict(source=self._source),
246
+ code=f"""
247
+ var indices = source.selected.indices;
248
+ var selected_ids = [];
249
+ for (var i = 0; i < indices.length; i++) {{
250
+ selected_ids.push(source.data['ids'][indices[i]]);
251
+ }}
252
+ var xhr = new XMLHttpRequest();
253
+ xhr.open("POST", "{route_path}", true);
254
+ xhr.setRequestHeader("Content-Type", "application/json");
255
+ xhr.send(JSON.stringify({{selected_ids: selected_ids}}));
256
+ """,
257
+ )
258
+ self._source.selected.js_on_change("indices", callback)
336
259
 
337
- def _process_plots(self, plots: List[Plot]) -> None:
338
- for plot in plots:
339
- renderer = plot.add(self._plot)
340
- plot.register(self.route_path)
341
- self._renderers.append(renderer)
260
+ def _get_html(self) -> str:
261
+ return f"""<div>
262
+ <script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-3.1.1.min.js"></script>
263
+ {self._script}
264
+ {self._div}
265
+ </div>"""
266
+
267
+ def _create_figure(self):
268
+ from bokeh.models import ( # pylint: disable=import-error
269
+ ColumnDataSource,
270
+ Legend,
271
+ )
272
+ from bokeh.plotting import figure # pylint: disable=import-error
273
+
274
+ self._plot = figure(width=self._width, height=self._height, tools=self._tools)
275
+
276
+ self._plot.xaxis.visible = self._x_axis_visible
277
+ self._plot.yaxis.visible = self._y_axis_visible
278
+ self._plot.grid.visible = self._grid_visible
342
279
 
343
- def _update_html(self) -> None:
280
+ self._source = ColumnDataSource(data=self._source_data)
281
+ self._add_callbacks()
282
+ legend_items = []
283
+ for plot in self._plots:
284
+ r = plot.add(self._plot, self._source)
285
+ legend_items.append((plot.name, [r]))
286
+
287
+ if self._show_legend:
288
+ self._plot.add_layout(
289
+ Legend(items=legend_items, click_policy=self._legend_click_policy),
290
+ self._legend_location,
291
+ )
292
+
293
+ def _update_html(self):
344
294
  from bokeh.embed import components # pylint: disable=import-error
345
295
 
346
- script, self._div = components(self._plot, wrap_script=False)
296
+ script, self._div = components(self._plot)
347
297
  self._div_id = self._get_div_id(self._div)
348
298
  self._script = self._update_script(script)
349
299
 
350
- @staticmethod
351
- def _generate_colors(x_coordinates: List[int], y_coordinates: List[int]) -> List[str]:
352
- colors = [
353
- "#%02x%02x%02x" % (int(r), int(g), 150)
354
- for r, g in zip(
355
- [50 + 2 * xi for xi in x_coordinates], [30 + 2 * yi for yi in y_coordinates]
356
- )
357
- ]
358
- return colors
359
-
360
- @staticmethod
361
- def _generate_radii(x_coordinates: List[int], y_coordinates: List[int]) -> List[int]:
362
- return [1] * len(x_coordinates)
300
+ def _update(self):
301
+ self._create_figure()
302
+ self._update_html()
363
303
 
364
304
  def _get_div_id(self, div: str) -> str:
365
305
  match = re.search(r'id="([^"]+)"', div)
@@ -368,69 +308,98 @@ class Bokeh(Widget):
368
308
  raise ValueError(f"No div id found in {div}")
369
309
 
370
310
  def _update_script(self, script: str) -> str:
371
- # TODO: Reimplement using regex.
372
311
  insert_after = "const fn = function() {"
373
312
  updated_script = ""
374
313
  for line in script.split("\n"):
375
314
  if line.strip().startswith(insert_after):
376
- line = line + f"\n const el = document.querySelector('#{self._div_id}');"
377
- line += "\n if (el === null) {"
378
- line += "\n setTimeout(fn, 500);"
379
- line += "\n return;"
380
- line += "\n }"
315
+ line += f"""
316
+ const el = document.querySelector('#{self._div_id}');
317
+ if (el === null) {{
318
+ setTimeout(fn, 200);
319
+ return;
320
+ }}
321
+ """
381
322
  updated_script += line + "\n"
382
323
  return updated_script
383
324
 
384
- def get_json_data(self):
385
- return {}
386
-
387
- def get_json_state(self):
388
- return {}
389
-
390
325
  def value_changed(self, func: Callable) -> Callable:
326
+ """Registers a callback function that will be called when the chart is clicked."""
327
+
391
328
  server = self._sly_app.get_server()
329
+ route_path = self.get_route_path(Bokeh.Routes.VALUE_CHANGED)
392
330
  self._changes_handled = True
393
- debounced_handler = DebouncedEventHandler(debounce_time=0.2) # TODO: check if it's enough
331
+ debounced_handler = DebouncedEventHandler(debounce_time=0.2)
394
332
 
395
- @server.post(self.route_path)
333
+ @server.post(route_path)
396
334
  async def _click(data: SelectedIds) -> None:
397
- debounced_handler.handle_event(data, func)
335
+ debounced_handler.handle_event(data.selected_ids, func)
398
336
 
399
337
  return _click
400
338
 
401
- def get_html(self) -> str:
402
- return f"""<div>
403
- <script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-3.1.1.min.js"></script>
404
- <script type="text/javascript"> {self._script} </script>
405
- {self._div}
406
- </div>"""
339
+ def clear(self) -> None:
340
+ """Clears all data in the ColumnDataSource and removes plots."""
407
341
 
408
- def update_radii(self, new_radii: Union[List[Union[list, int, float]], int, float]) -> None:
409
- if isinstance(new_radii, (int, float)):
410
- new_radii = [new_radii] * len(self._plots)
411
- elif len(new_radii) != len(self._plots):
412
- logger.warning(
413
- f"{len(new_radii)} != {len(self._plots)}: new_radii will be broadcasted to all plots"
414
- )
415
- new_radii = [new_radii[0]] * len(self._plots)
416
- for idx, radii in enumerate(new_radii):
417
- self.update_radii_by_plot_idx(idx, radii)
418
-
419
- def update_radii_by_plot_idx(self, plot_idx: int, new_radii: List[Union[int, float]]) -> None:
420
- coords_length = len(self._plots[plot_idx]._x_coordinates)
421
- if isinstance(new_radii, (int, float)):
422
- new_radii = [new_radii] * coords_length
423
- elif len(new_radii) != coords_length:
424
- logger.warning(
425
- f"{len(new_radii)} != {coords_length}: new_radii will be broadcasted to all plots"
426
- )
427
- new_radii = [new_radii[0]] * coords_length
342
+ self._source_data = {key: [] for key in self._source_data.keys()}
343
+ self._plots = []
344
+ self._update()
345
+
346
+ def refresh(self) -> None:
347
+ """Refreshes the chart by reloading the existing data and updating the HTML."""
348
+
349
+ self._update()
350
+
351
+ def update_chart_size(self, width: Optional[int] = None, height: Optional[int] = None):
352
+ """Updates the size of the chart."""
353
+
354
+ if width:
355
+ self._width = width
356
+ if height:
357
+ self._height = height
358
+ self._update()
359
+
360
+ def add_data(
361
+ self,
362
+ x: List[float],
363
+ y: List[float],
364
+ radius: List[float],
365
+ colors: List[str],
366
+ ids: List[Any],
367
+ names: List[str],
368
+ append: bool = True,
369
+ ):
370
+ """Adds data to the chart."""
371
+
372
+ if append:
373
+ self._source.data["x"] += x
374
+ self._source.data["y"] += y
375
+ self._source.data["radius"] += radius
376
+ self._source.data["colors"] += colors
377
+ self._source.data["ids"] += ids
378
+ self._source.data["names"] += names
379
+ else:
380
+ self._source.data = {
381
+ "x": x,
382
+ "y": y,
383
+ "radius": radius,
384
+ "colors": colors,
385
+ "ids": ids,
386
+ "names": names,
387
+ }
388
+
389
+ def add_plot(self, plot: Plot):
390
+ """Adds a plot to the chart."""
391
+
392
+ self._plots.append(plot)
393
+ self._update()
394
+
395
+ def add_plots(self, plots: List[Plot]):
396
+ """Adds multiple plots to the chart."""
397
+
398
+ self._plots.extend(plots)
399
+ self._update()
428
400
 
429
- self._plots[plot_idx]._radii = new_radii
430
- self._plots[plot_idx]._source.data["radius"] = new_radii
431
- self._load_chart()
401
+ def update_point_size(self, size: float):
402
+ """Updates the size of the points in the chart."""
432
403
 
433
- def update_chart_size(self, width: Optional[int] = None, height: Optional[int] = None) -> None:
434
- self._width = width or self._width
435
- self._height = height or self._height
436
- self._load_chart()
404
+ self._source_data["radius"] = [size] * len(self._source_data["x"])
405
+ self._update()
@@ -9,6 +9,7 @@ from typing import Dict, List, Optional, Tuple, Union
9
9
  import cv2
10
10
  import numpy as np
11
11
 
12
+ from supervisely import logger
12
13
  from supervisely.geometry.constants import (
13
14
  CLASS_ID,
14
15
  CREATED_AT,
@@ -215,6 +216,8 @@ class GraphNodes(Geometry):
215
216
  updated_at=updated_at,
216
217
  created_at=created_at,
217
218
  )
219
+ if len(nodes) == 0:
220
+ raise ValueError("Empty list of nodes is not allowed for GraphNodes")
218
221
  self._nodes = nodes
219
222
  if isinstance(nodes, (list, tuple)):
220
223
  self._nodes = {}
@@ -593,6 +596,10 @@ class GraphNodes(Geometry):
593
596
 
594
597
  rectangle = figure.to_bbox()
595
598
  """
599
+ if self._nodes is None or len(self._nodes) == 0:
600
+ logger.warning(
601
+ f"Cannot create a bounding box from {self.name()} with empty nodes. Geometry ID: {self.sly_id} "
602
+ )
596
603
  return Rectangle.from_geometries_list(
597
604
  [Point.from_point_location(node.location) for node in self._nodes.values()]
598
605
  )
@@ -476,7 +476,7 @@ class Rectangle(Geometry):
476
476
  return cls(0, 0, size[0] - 1, size[1] - 1)
477
477
 
478
478
  @classmethod
479
- def from_geometries_list(cls, geometries: List[sly.geometry.geometry]) -> Rectangle:
479
+ def from_geometries_list(cls, geometries: List[Geometry]) -> Rectangle:
480
480
  """
481
481
  Create Rectangle from given geometry objects.
482
482
 
@@ -494,6 +494,8 @@ class Rectangle(Geometry):
494
494
  geom_objs = [sly.Point(100, 200), sly.Polyline([sly.PointLocation(730, 2104), sly.PointLocation(2479, 402)])]
495
495
  figure_from_geom_objs = sly.Rectangle.from_geometries_list(geom_objs)
496
496
  """
497
+ if geometries is None or len(geometries) == 0:
498
+ raise ValueError("No geometries provided to create a Rectangle.")
497
499
  bboxes = [g.to_bbox() for g in geometries]
498
500
  top = min(bbox.top for bbox in bboxes)
499
501
  left = min(bbox.left for bbox in bboxes)
@@ -4780,26 +4780,6 @@ async def _download_project_async(
4780
4780
  if semaphore is None:
4781
4781
  semaphore = api.get_default_semaphore()
4782
4782
 
4783
- # number of workers
4784
- num_workers = min(kwargs.get("num_workers", semaphore._value), 10)
4785
-
4786
- async def worker(queue: asyncio.Queue, stop_event: asyncio.Event):
4787
- while not stop_event.is_set():
4788
- task = await queue.get()
4789
- if task is None:
4790
- break
4791
- try:
4792
- await task
4793
- except Exception as e:
4794
- logger.error(f"Error in _download_project_async worker: {e}")
4795
- stop_event.set()
4796
- finally:
4797
- queue.task_done()
4798
-
4799
- queue = asyncio.Queue()
4800
- stop_event = asyncio.Event()
4801
- workers = [asyncio.create_task(worker(queue, stop_event)) for _ in range(num_workers)]
4802
-
4803
4783
  dataset_ids = set(dataset_ids) if (dataset_ids is not None) else None
4804
4784
  project_fs = None
4805
4785
  meta = ProjectMeta.from_json(api.project.get_meta(project_id, with_settings=True))
@@ -4883,11 +4863,25 @@ async def _download_project_async(
4883
4863
  ds_progress(1)
4884
4864
  return to_download
4885
4865
 
4866
+ async def run_tasks_with_delay(tasks, delay=0.1):
4867
+ created_tasks = []
4868
+ for task in tasks:
4869
+ created_task = asyncio.create_task(task)
4870
+ created_tasks.append(created_task)
4871
+ await asyncio.sleep(delay)
4872
+ logger.debug(
4873
+ f"{len(created_tasks)} tasks have been created for dataset ID: {dataset.id}, Name: {dataset.name}"
4874
+ )
4875
+ return created_tasks
4876
+
4877
+ tasks = []
4886
4878
  small_images = await check_items(small_images)
4887
4879
  large_images = await check_items(large_images)
4880
+
4888
4881
  if len(small_images) == 1:
4889
4882
  large_images.append(small_images.pop())
4890
4883
  for images_batch in batched(small_images, batch_size=batch_size):
4884
+
4891
4885
  task = _download_project_items_batch_async(
4892
4886
  api=api,
4893
4887
  dataset_id=dataset_id,
@@ -4901,7 +4895,7 @@ async def _download_project_async(
4901
4895
  only_image_tags=only_image_tags,
4902
4896
  progress_cb=ds_progress,
4903
4897
  )
4904
- await queue.put(task)
4898
+ tasks.append(task)
4905
4899
  for image in large_images:
4906
4900
  task = _download_project_item_async(
4907
4901
  api=api,
@@ -4915,9 +4909,10 @@ async def _download_project_async(
4915
4909
  only_image_tags=only_image_tags,
4916
4910
  progress_cb=ds_progress,
4917
4911
  )
4918
- await queue.put(task)
4912
+ tasks.append(task)
4919
4913
 
4920
- await queue.join()
4914
+ created_tasks = await run_tasks_with_delay(tasks)
4915
+ await asyncio.gather(*created_tasks)
4921
4916
 
4922
4917
  if save_image_meta:
4923
4918
  meta_dir = dataset_fs.meta_dir
@@ -4934,13 +4929,6 @@ async def _download_project_async(
4934
4929
  if item_name not in items_names_set:
4935
4930
  dataset_fs.delete_item(item_name)
4936
4931
 
4937
- for _ in range(num_workers):
4938
- await queue.put(None)
4939
- await asyncio.gather(*workers)
4940
-
4941
- if stop_event.is_set():
4942
- raise RuntimeError("Download process was stopped due to an error in one of the workers.")
4943
-
4944
4932
  try:
4945
4933
  create_readme(dest_dir, project_id, api)
4946
4934
  except Exception as e:
@@ -4964,7 +4952,7 @@ async def _download_project_item_async(
4964
4952
  """
4965
4953
  if save_images:
4966
4954
  logger.debug(
4967
- f"Downloading 1 image in single mode: {img_info.name} with _download_project_item_async"
4955
+ f"Downloading 1 image in single mode with _download_project_item_async. ID: {img_info.id}, Name: {img_info.name}"
4968
4956
  )
4969
4957
  img_bytes = await api.image.download_bytes_single_async(
4970
4958
  img_info.id, semaphore=semaphore, check_hash=True
@@ -4982,7 +4970,11 @@ async def _download_project_item_async(
4982
4970
  force_metadata_for_links=not save_images,
4983
4971
  )
4984
4972
  ann_json = ann_info.annotation
4985
- tmp_ann = Annotation.from_json(ann_json, meta)
4973
+ try:
4974
+ tmp_ann = Annotation.from_json(ann_json, meta)
4975
+ except Exception:
4976
+ logger.error(f"Error while deserializing annotation for image with ID: {img_info.id}")
4977
+ raise
4986
4978
  if None in tmp_ann.img_size:
4987
4979
  tmp_ann = tmp_ann.clone(img_size=(img_info.height, img_info.width))
4988
4980
  ann_json = tmp_ann.to_json()
@@ -5004,6 +4996,7 @@ async def _download_project_item_async(
5004
4996
  )
5005
4997
  if progress_cb is not None:
5006
4998
  progress_cb(1)
4999
+ logger.debug(f"Single project item has been downloaded. Semaphore state: {semaphore._value}")
5007
5000
 
5008
5001
 
5009
5002
  async def _download_project_items_batch_async(
@@ -5056,12 +5049,18 @@ async def _download_project_items_batch_async(
5056
5049
  semaphore=semaphore,
5057
5050
  force_metadata_for_links=not save_images,
5058
5051
  )
5059
- tmps_anns = [Annotation.from_json(ann_info.annotation, meta) for ann_info in ann_infos]
5060
5052
  ann_jsons = []
5061
- for tmp_ann in tmps_anns:
5062
- if None in tmp_ann.img_size:
5063
- tmp_ann = tmp_ann.clone(img_size=(img_info.height, img_info.width))
5064
- ann_jsons.append(tmp_ann.to_json())
5053
+ for img_info, ann_info in zip(img_infos, ann_infos):
5054
+ try:
5055
+ tmp_ann = Annotation.from_json(ann_info.annotation, meta)
5056
+ if None in tmp_ann.img_size:
5057
+ tmp_ann = tmp_ann.clone(img_size=(img_info.height, img_info.width))
5058
+ ann_jsons.append(tmp_ann.to_json())
5059
+ except Exception:
5060
+ logger.error(
5061
+ f"Error while deserializing annotation for image with ID: {img_info.id}"
5062
+ )
5063
+ raise
5065
5064
  else:
5066
5065
  ann_jsons = []
5067
5066
  for img_info in img_infos:
@@ -5083,5 +5082,7 @@ async def _download_project_items_batch_async(
5083
5082
  if progress_cb is not None:
5084
5083
  progress_cb(1)
5085
5084
 
5085
+ logger.debug(f"Batch of project items has been downloaded. Semaphore state: {semaphore._value}")
5086
+
5086
5087
 
5087
5088
  DatasetDict = Project.DatasetDict
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: supervisely
3
- Version: 6.73.287
3
+ Version: 6.73.289
4
4
  Summary: Supervisely Python SDK.
5
5
  Home-page: https://github.com/supervisely/supervisely
6
6
  Author: Supervisely
@@ -5,7 +5,7 @@ supervisely/function_wrapper.py,sha256=R5YajTQ0GnRp2vtjwfC9hINkzQc0JiyGsu8TER373
5
5
  supervisely/sly_logger.py,sha256=LG1wTyyctyEKuCuKM2IKf_SMPH7BzkTsFdO-0tnorzg,6225
6
6
  supervisely/tiny_timer.py,sha256=hkpe_7FE6bsKL79blSs7WBaktuPavEVu67IpEPrfmjE,183
7
7
  supervisely/annotation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- supervisely/annotation/annotation.py,sha256=Kdn3HRpx7ie6vkDaQFXrg597nOidZ6FMN-oXpDk4nyI,114289
8
+ supervisely/annotation/annotation.py,sha256=5AG1AhebkmiYy2r7nKbz6TjdmCF4tuf9FtqUjLLs7aU,114659
9
9
  supervisely/annotation/annotation_transforms.py,sha256=TlVy_gUbM-XH6GbLpZPrAi6pMIGTr7Ow02iSKOSTa-I,9582
10
10
  supervisely/annotation/json_geometries_map.py,sha256=nL6AmMhFy02fw9ryBm75plKyOkDh61QdOToSuLAcz_Q,1659
11
11
  supervisely/annotation/label.py,sha256=NpHZ5o2H6dI4KiII22o2HpiLXG1yekh-bEy8WvI2Ljg,37498
@@ -21,14 +21,14 @@ supervisely/annotation/tag_meta_mapper.py,sha256=RWeTrxJ64syodyhXIRSH007bX6Hr3B4
21
21
  supervisely/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  supervisely/api/advanced_api.py,sha256=Nd5cCnHFWc3PSUrCtENxTGtDjS37_lCHXsgXvUI3Ti8,2054
23
23
  supervisely/api/agent_api.py,sha256=ShWAIlXcWXcyI9fqVuP5GZVCigCMJmjnvdGUfLspD6Y,8890
24
- supervisely/api/annotation_api.py,sha256=kB9l0NhQEkunGDC9fWjNzf5DdhqRF1tv-RRnIbkV2k0,64941
25
- supervisely/api/api.py,sha256=0dgPx_eizoCEFzfT8YH9uh1kq-OJwjrV5fBGD7uZ7E4,65840
24
+ supervisely/api/annotation_api.py,sha256=fVQJOg5SLcD_mRUmPaVsJIOVTGFhsabRXqve0LyUgrc,64743
25
+ supervisely/api/api.py,sha256=YBE6yi682H5dy3BBQtESmfC9hKZcbHyYRPNGLRldgSU,66014
26
26
  supervisely/api/app_api.py,sha256=RsbVej8WxWVn9cNo5s3Fqd1symsCdsfOaKVBKEUapRY,71927
27
27
  supervisely/api/dataset_api.py,sha256=GH7prDRJKyJlTv_7_Y-RkTwJN7ED4EkXNqqmi3iIdI4,41352
28
28
  supervisely/api/file_api.py,sha256=v2FsD3oljwNPqcDgEJRe8Bu5k0PYKzVhqmRb5QFaHAQ,83422
29
29
  supervisely/api/github_api.py,sha256=NIexNjEer9H5rf5sw2LEZd7C1WR-tK4t6IZzsgeAAwQ,623
30
30
  supervisely/api/image_annotation_tool_api.py,sha256=YcUo78jRDBJYvIjrd-Y6FJAasLta54nnxhyaGyanovA,5237
31
- supervisely/api/image_api.py,sha256=qZwTjeCo6bkEuXDuB8RhhP0g6PzlRuCXJkUfN9rsUZ4,190985
31
+ supervisely/api/image_api.py,sha256=bSal6vB2c7Ct2qDarXTaTmXy7x0X1VlV8oTuT6YpY2o,191061
32
32
  supervisely/api/import_storage_api.py,sha256=BDCgmR0Hv6OoiRHLCVPKt3iDxSVlQp1WrnKhAK_Zl84,460
33
33
  supervisely/api/issues_api.py,sha256=BqDJXmNoTzwc3xe6_-mA7FDFC5QQ-ahGbXk_HmpkSeQ,17925
34
34
  supervisely/api/labeling_job_api.py,sha256=odnzZjp29yM16Gq-FYkv-OA4WFMNJCLFo4qSikW2A7c,56280
@@ -49,7 +49,7 @@ supervisely/api/video_annotation_tool_api.py,sha256=3A9-U8WJzrTShP_n9T8U01M9FzGY
49
49
  supervisely/api/workspace_api.py,sha256=5KAxpI9DKBmgF_pyQaXHpGT30HZ9wRtR6DP3FoYFZtY,9228
50
50
  supervisely/api/entity_annotation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
51
  supervisely/api/entity_annotation/entity_annotation_api.py,sha256=K79KdDyepQv4FiNQHBj9V4-zLIemxK9WG1ig1bfBKb8,3083
52
- supervisely/api/entity_annotation/figure_api.py,sha256=deYCZNG7JeDhxlYew51FyGvqY3dc7fkERtwmBPJmHcw,24503
52
+ supervisely/api/entity_annotation/figure_api.py,sha256=jNObHAjy2JdXvKLP5IeBWISDjrZn_Budxp9J3Odyhxo,24531
53
53
  supervisely/api/entity_annotation/object_api.py,sha256=gbcNvN_KY6G80Me8fHKQgryc2Co7VU_kfFd1GYILZ4E,8875
54
54
  supervisely/api/entity_annotation/tag_api.py,sha256=M-28m9h8R4k9Eqo6P1S0UH8_D5kqCwAvQLYY6_Yz4oM,11161
55
55
  supervisely/api/pointcloud/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -129,7 +129,7 @@ supervisely/app/widgets/binded_input_number/__init__.py,sha256=47DEQpj8HBSa-_TIm
129
129
  supervisely/app/widgets/binded_input_number/binded_input_number.py,sha256=RXTAGaMXtGOl4pprVLyQosr61t2rI_U_U8VNHhSyBhA,6251
130
130
  supervisely/app/widgets/binded_input_number/template.html,sha256=uCEZ54BNC2itr39wxxThXw62WlJ9659cuz8osCm0WZE,162
131
131
  supervisely/app/widgets/bokeh/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
132
- supervisely/app/widgets/bokeh/bokeh.py,sha256=zob2F-cJ6JX_Dk2FANfN94uHDyxvgoxuBpeL0OebVyg,16128
132
+ supervisely/app/widgets/bokeh/bokeh.py,sha256=H53AqKc3sGJU3aoYeIdFfJoeIquyxbIHCDV8CRBjrUs,13262
133
133
  supervisely/app/widgets/bokeh/template.html,sha256=ntsh7xx4q9OHG62sa_r3INDxsXgvdPFIWTtYaWn_0t8,12
134
134
  supervisely/app/widgets/button/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
135
135
  supervisely/app/widgets/button/button.py,sha256=zDQinhOOjNNxdP2GIFrwTmVfGAeJJoKV6CT6C8KzQNI,10405
@@ -677,7 +677,7 @@ supervisely/geometry/cuboid.py,sha256=oxsRoTKuwTNxH4Vp6khyvw1TCrBagSWNV5HmQKJZHt
677
677
  supervisely/geometry/cuboid_2d.py,sha256=-oXeKiUS2gguQ4GyIZYp1cNPPhOLsGOFZl7uI71BfZM,13438
678
678
  supervisely/geometry/cuboid_3d.py,sha256=x472ZPHTZDIY5Dj8tKbLQG3BCukFPgSvPJlxfHdKi1w,4168
679
679
  supervisely/geometry/geometry.py,sha256=dbXnct8hrr7Wour6yCrtAef22KSJ2uYRm1F5GE10_MM,15287
680
- supervisely/geometry/graph.py,sha256=1_tX7FGmYkXuAx3P_w4zA4p8mRYnCN4iZ--2pMyxseI,24121
680
+ supervisely/geometry/graph.py,sha256=RDdZtN_P7TKAg4s_QXluCGzdmhD-IeonvK4Pix924kk,24474
681
681
  supervisely/geometry/helpers.py,sha256=2gdYMFWTAr836gVXcp-lkDQs9tdaV0ou33kj3mzJBQA,5132
682
682
  supervisely/geometry/image_rotator.py,sha256=wrU8cXEUfuNcmPms2myUV4BpZqz_2oDArsEUFeiTpxs,6888
683
683
  supervisely/geometry/main_tests.py,sha256=K3Olsz9igHDW2IfIA5JOpjoE8bZ3ex2PXvVR2ZCDrHU,27199
@@ -689,7 +689,7 @@ supervisely/geometry/point_location.py,sha256=vLu5pWdtAi-WVQUKgFO7skigTaR-mtWR0t
689
689
  supervisely/geometry/pointcloud.py,sha256=cc4P_UNLGx5dWah3caRJytW7_mAi8UnYsJOa20mUy8s,1472
690
690
  supervisely/geometry/polygon.py,sha256=cAgCR8ccdGtieJTnmDnupPALMEwerHIqMWx7k3OCzVQ,11594
691
691
  supervisely/geometry/polyline.py,sha256=LjjD-YGVDw1TQ84_IOHqnq43JFuSnsGdGMx404olYDs,8258
692
- supervisely/geometry/rectangle.py,sha256=f-Y6AnVYbMXXaAOLREyjqVJeb-l_tevQQHy9kiMKHhI,33749
692
+ supervisely/geometry/rectangle.py,sha256=QaBcSPeH87rcwsSft1TavEdCe4NpvfHZztZMEmzIxGk,33869
693
693
  supervisely/geometry/sliding_windows.py,sha256=VWtE3DS9AaIlS0ch0PY6wwtWU89J82icDRZ-F0LFrjM,1700
694
694
  supervisely/geometry/sliding_windows_fuzzy.py,sha256=InvJlH6MEW55DM1IdoMHP2MLFLieTDZfHrZZEINLQOc,3626
695
695
  supervisely/geometry/validation.py,sha256=G5vjtiXTCaTQvWegPIBiNw8pN_GiY86OUSRSsccdyLU,2139
@@ -1009,7 +1009,7 @@ supervisely/project/data_version.py,sha256=nknaWJSUCwoDyNG9_d1KA-GjzidhV9zd9Cn8c
1009
1009
  supervisely/project/download.py,sha256=zb8sb4XZ6Qi3CP7fmtLRUAYzaxs_W0WnOfe2x3ZVRMs,24639
1010
1010
  supervisely/project/pointcloud_episode_project.py,sha256=yiWdNBQiI6f1O9sr1pg8JHW6O-w3XUB1rikJNn3Oung,41866
1011
1011
  supervisely/project/pointcloud_project.py,sha256=Kx1Vaes-krwG3BiRRtHRLQxb9G5m5bTHPN9IzRqmNWo,49399
1012
- supervisely/project/project.py,sha256=tvNPGyIZVs4p3iMz2eDU1tmtsPZWZOhQ9vBJCqCMxbs,202003
1012
+ supervisely/project/project.py,sha256=34fAbYV4VdfVSqMs0a5ggAIwELd8nPb-uGoaC1F7h4I,202299
1013
1013
  supervisely/project/project_meta.py,sha256=26s8IiHC5Pg8B1AQi6_CrsWteioJP2in00cRNe8QlW0,51423
1014
1014
  supervisely/project/project_settings.py,sha256=NLThzU_DCynOK6hkHhVdFyezwprn9UqlnrLDe_3qhkY,9347
1015
1015
  supervisely/project/project_type.py,sha256=_3RqW2CnDBKFOvSIrQT1RJQaiHirs34_jiQS8CkwCpo,530
@@ -1071,9 +1071,9 @@ supervisely/worker_proto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
1071
1071
  supervisely/worker_proto/worker_api_pb2.py,sha256=VQfi5JRBHs2pFCK1snec3JECgGnua3Xjqw_-b3aFxuM,59142
1072
1072
  supervisely/worker_proto/worker_api_pb2_grpc.py,sha256=3BwQXOaP9qpdi0Dt9EKG--Lm8KGN0C5AgmUfRv77_Jk,28940
1073
1073
  supervisely_lib/__init__.py,sha256=7-3QnN8Zf0wj8NCr2oJmqoQWMKKPKTECvjH9pd2S5vY,159
1074
- supervisely-6.73.287.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
1075
- supervisely-6.73.287.dist-info/METADATA,sha256=9oifX7yDdPoCm5GaVVjDyURQrPfVIjGIT27n3C10lf8,33573
1076
- supervisely-6.73.287.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
1077
- supervisely-6.73.287.dist-info/entry_points.txt,sha256=U96-5Hxrp2ApRjnCoUiUhWMqijqh8zLR03sEhWtAcms,102
1078
- supervisely-6.73.287.dist-info/top_level.txt,sha256=kcFVwb7SXtfqZifrZaSE3owHExX4gcNYe7Q2uoby084,28
1079
- supervisely-6.73.287.dist-info/RECORD,,
1074
+ supervisely-6.73.289.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
1075
+ supervisely-6.73.289.dist-info/METADATA,sha256=RMgZIP_bwLgos0vF9tP2PFNzmKVI5h1VFkdK_bX7WKE,33573
1076
+ supervisely-6.73.289.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
1077
+ supervisely-6.73.289.dist-info/entry_points.txt,sha256=U96-5Hxrp2ApRjnCoUiUhWMqijqh8zLR03sEhWtAcms,102
1078
+ supervisely-6.73.289.dist-info/top_level.txt,sha256=kcFVwb7SXtfqZifrZaSE3owHExX4gcNYe7Q2uoby084,28
1079
+ supervisely-6.73.289.dist-info/RECORD,,