supervisely 6.73.226__py3-none-any.whl → 6.73.228__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 CHANGED
@@ -55,7 +55,7 @@ from supervisely.task.progress import (
55
55
 
56
56
  import supervisely.project as project
57
57
  from supervisely.project import read_project, get_project_class
58
- from supervisely.project.download import download
58
+ from supervisely.project.download import download, download_async
59
59
  from supervisely.project.upload import upload
60
60
  from supervisely.project.project import (
61
61
  Project,
@@ -309,4 +309,4 @@ except Exception as e:
309
309
  # If new changes in Supervisely Python SDK require upgrade of the Supervisely instance
310
310
  # set a new value for the environment variable MINIMUM_INSTANCE_VERSION_FOR_SDK, otherwise
311
311
  # users can face compatibility issues, if the instance version is lower than the SDK version.
312
- os.environ["MINIMUM_INSTANCE_VERSION_FOR_SDK"] = "6.11.16"
312
+ os.environ["MINIMUM_INSTANCE_VERSION_FOR_SDK"] = "6.12.5"
supervisely/_utils.py CHANGED
@@ -1,5 +1,6 @@
1
1
  # coding: utf-8
2
2
 
3
+ import asyncio
3
4
  import base64
4
5
  import copy
5
6
  import hashlib
@@ -13,7 +14,7 @@ import urllib
13
14
  from datetime import datetime
14
15
  from functools import wraps
15
16
  from tempfile import gettempdir
16
- from typing import List, Literal, Optional
17
+ from typing import Any, Dict, List, Literal, Optional, Tuple
17
18
 
18
19
  import numpy as np
19
20
  from requests.utils import DEFAULT_CA_BUNDLE_PATH
@@ -315,6 +316,15 @@ def get_readable_datetime(value: str) -> str:
315
316
  return dt.strftime("%Y-%m-%d %H:%M:%S")
316
317
 
317
318
 
319
+ def get_unix_timestamp() -> int:
320
+ """Return the current Unix timestamp.
321
+
322
+ :return: Current Unix timestamp.
323
+ :rtype: int
324
+ """
325
+ return int(time.time())
326
+
327
+
318
328
  def get_certificates_list(path: str = DEFAULT_CA_BUNDLE_PATH) -> List[str]:
319
329
  with open(path, "r", encoding="ascii") as f:
320
330
  content = f.read().strip()
@@ -382,3 +392,70 @@ def add_callback(func, callback):
382
392
  return res
383
393
 
384
394
  return wrapper
395
+
396
+
397
+ def compare_dicts(
398
+ template: Dict[Any, Any], data: Dict[Any, Any], strict: bool = True
399
+ ) -> Tuple[List[str], List[str]]:
400
+ """Compare two dictionaries recursively (by keys only) and return lists of missing and extra fields.
401
+ If strict is True, the keys of the template and data dictionaries must match exactly.
402
+ Otherwise, the data dictionary may contain additional keys that are not in the template dictionary.
403
+
404
+ :param template: The template dictionary.
405
+ :type template: Dict[Any, Any]
406
+ :param data: The data dictionary.
407
+ :type data: Dict[Any, Any]
408
+ :param strict: If True, the keys of the template and data dictionaries must match exactly.
409
+ :type strict: bool, optional
410
+ :return: A tuple containing a list of missing fields and a list of extra fields.
411
+ :rtype: Tuple[List[str], List[str]]
412
+ """
413
+ missing_fields = []
414
+ extra_fields = []
415
+
416
+ if not isinstance(template, dict) or not isinstance(data, dict):
417
+ return missing_fields, extra_fields
418
+
419
+ if strict:
420
+ template_keys = set(template.keys())
421
+ data_keys = set(data.keys())
422
+
423
+ missing_fields = list(template_keys - data_keys)
424
+ extra_fields = list(data_keys - template_keys)
425
+
426
+ for key in template_keys & data_keys:
427
+ sub_missing, sub_extra = compare_dicts(template[key], data[key], strict)
428
+ missing_fields.extend([f"{key}.{m}" for m in sub_missing])
429
+ extra_fields.extend([f"{key}.{e}" for e in sub_extra])
430
+ else:
431
+ for key in template:
432
+ if key not in data:
433
+ missing_fields.append(key)
434
+ else:
435
+ sub_missing, sub_extra = compare_dicts(template[key], data[key], strict)
436
+ missing_fields.extend([f"{key}.{m}" for m in sub_missing])
437
+ extra_fields.extend([f"{key}.{e}" for e in sub_extra])
438
+
439
+ return missing_fields, extra_fields
440
+
441
+
442
+ def get_or_create_event_loop() -> asyncio.AbstractEventLoop:
443
+ """
444
+ Get the current event loop or create a new one if it doesn't exist.
445
+ Works for different Python versions and contexts.
446
+
447
+ :return: Event loop
448
+ :rtype: asyncio.AbstractEventLoop
449
+ """
450
+ try:
451
+ # Preferred method for asynchronous context (Python 3.7+)
452
+ return asyncio.get_running_loop()
453
+ except RuntimeError:
454
+ # If the loop is not running, get the current one or create a new one (Python 3.8 and 3.9)
455
+ try:
456
+ return asyncio.get_event_loop()
457
+ except RuntimeError:
458
+ # For Python 3.10+ or if the call occurs outside of an active loop context
459
+ loop = asyncio.new_event_loop()
460
+ asyncio.set_event_loop(loop)
461
+ return loop
@@ -4,10 +4,11 @@
4
4
  # docs
5
5
  from __future__ import annotations
6
6
 
7
+ import asyncio
7
8
  import json
8
9
  from collections import defaultdict
9
10
  from copy import deepcopy
10
- from typing import Any, Callable, Dict, List, NamedTuple, Optional, Union
11
+ from typing import Any, Callable, Dict, List, Literal, NamedTuple, Optional, Union
11
12
 
12
13
  from tqdm import tqdm
13
14
 
@@ -1307,36 +1308,36 @@ class AnnotationApi(ModuleApi):
1307
1308
  Priority increases with the number: a higher number indicates a higher priority.
1308
1309
  The higher priority means that the label will be displayed on top of the others.
1309
1310
  The lower priority means that the label will be displayed below the others.
1310
-
1311
+
1311
1312
  :param label_id: ID of the label to update
1312
1313
  :type label_id: int
1313
1314
  :param priority: New priority of the label
1314
1315
  :type priority: int
1315
-
1316
- :Usage example:
1317
-
1316
+
1317
+ :Usage example:
1318
+
1318
1319
  .. code-block:: python
1319
-
1320
+
1320
1321
  import os
1321
1322
  from dotenv import load_dotenv
1322
-
1323
+
1323
1324
  import supervisely as sly
1324
-
1325
+
1325
1326
  # Load secrets and create API object from .env file (recommended)
1326
1327
  # Learn more here: https://developer.supervisely.com/getting-started/basics-of-authentication
1327
1328
  load_dotenv(os.path.expanduser("~/supervisely.env"))
1328
-
1329
+
1329
1330
  api = sly.Api.from_env()
1330
-
1331
+
1331
1332
  label_ids = [123, 456, 789]
1332
1333
  priorities = [1, 2, 3]
1333
-
1334
+
1334
1335
  for label_id, priority in zip(label_ids, priorities):
1335
1336
  api.annotation.update_label_priority(label_id, priority)
1336
-
1337
+
1337
1338
  # The label with ID 789 will be displayed on top of the others.
1338
1339
  # The label with ID 123 will be displayed below the others.
1339
-
1340
+
1340
1341
  """
1341
1342
  self._api.post(
1342
1343
  "figures.priority.update",
@@ -1344,4 +1345,173 @@ class AnnotationApi(ModuleApi):
1344
1345
  ApiField.ID: label_id,
1345
1346
  ApiField.PRIORITY: priority,
1346
1347
  },
1347
- )
1348
+ )
1349
+
1350
+ async def download_async(
1351
+ self,
1352
+ image_id: int,
1353
+ semaphore: Optional[asyncio.Semaphore] = None,
1354
+ with_custom_data: Optional[bool] = False,
1355
+ force_metadata_for_links: Optional[bool] = True,
1356
+ progress_cb: Optional[Union[tqdm, Callable]] = None,
1357
+ progress_cb_type: Literal["number", "size"] = "number",
1358
+ ) -> AnnotationInfo:
1359
+ """
1360
+ Download AnnotationInfo by image ID from API.
1361
+
1362
+ :param image_id: Image ID in Supervisely.
1363
+ :type image_id: int
1364
+ :param semaphore: Semaphore for limiting the number of simultaneous downloads.
1365
+ :type semaphore: asyncio.Semaphore, optional
1366
+ :param with_custom_data: Include custom data in the response.
1367
+ :type with_custom_data: bool, optional
1368
+ :param force_metadata_for_links: Force metadata for links.
1369
+ :type force_metadata_for_links: bool, optional
1370
+ :param progress_cb: Function for tracking download progress.
1371
+ :type progress_cb: tqdm or callable, optional
1372
+ :param progress_cb_type: Type of progress callback. Can be "number" or "size". Default is "number".
1373
+ :type progress_cb_type: str, optional
1374
+ :return: Information about Annotation. See :class:`info_sequence<info_sequence>`
1375
+ :rtype: :class:`AnnotationInfo`
1376
+ :Usage example:
1377
+
1378
+ .. code-block:: python
1379
+
1380
+ import supervisely as sly
1381
+
1382
+ os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
1383
+ os.environ['API_TOKEN'] = 'Your Supervisely API Token'
1384
+ api = sly.Api.from_env()
1385
+
1386
+ image_id = 121236918
1387
+ loop = sly.utils.get_or_create_event_loop()
1388
+ ann_info = loop.run_until_complete(api.annotation.download_async(image_id))
1389
+ """
1390
+ if semaphore is None:
1391
+ semaphore = self._api._get_default_semaphore()
1392
+ async with semaphore:
1393
+ response = await self._api.post_async(
1394
+ "annotations.info",
1395
+ {
1396
+ ApiField.IMAGE_ID: image_id,
1397
+ ApiField.WITH_CUSTOM_DATA: with_custom_data,
1398
+ ApiField.FORCE_METADATA_FOR_LINKS: force_metadata_for_links,
1399
+ ApiField.INTEGER_COORDS: False,
1400
+ },
1401
+ )
1402
+ if progress_cb is not None and progress_cb_type == "size":
1403
+ progress_cb(len(response.content))
1404
+
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]
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 = self._api.image.figure.download_geometries_batch(
1422
+ figure_ids,
1423
+ (
1424
+ progress_cb
1425
+ if progress_cb is not None and progress_cb_type == "size"
1426
+ else None
1427
+ ),
1428
+ )
1429
+ for figure_id, geometry in zip(figure_ids, figures):
1430
+ label_idx = additonal_geometries[figure_id]
1431
+ labels[label_idx].update({BITMAP: geometry})
1432
+ ann_info = self._convert_json_info(result)
1433
+ if progress_cb is not None and progress_cb_type == "number":
1434
+ progress_cb(1)
1435
+ return ann_info
1436
+
1437
+ async def download_batch_async(
1438
+ self,
1439
+ dataset_id: int,
1440
+ image_ids: List[int],
1441
+ semaphore: Optional[asyncio.Semaphore] = None,
1442
+ with_custom_data: Optional[bool] = False,
1443
+ force_metadata_for_links: Optional[bool] = True,
1444
+ progress_cb: Optional[Union[tqdm, Callable]] = None,
1445
+ progress_cb_type: Literal["number", "size"] = "number",
1446
+ ) -> List[AnnotationInfo]:
1447
+ """
1448
+ Get list of AnnotationInfos for given dataset ID from API.
1449
+
1450
+ :param dataset_id: Dataset ID in Supervisely.
1451
+ :type dataset_id: int
1452
+ :param image_ids: List of integers.
1453
+ :type image_ids: List[int]
1454
+ :param semaphore: Semaphore for limiting the number of simultaneous downloads.
1455
+ :type semaphore: asyncio.Semaphore, optional
1456
+ :param with_custom_data: Include custom data in the response.
1457
+ :type with_custom_data: bool, optional
1458
+ :param force_metadata_for_links: Force metadata for links.
1459
+ :type force_metadata_for_links: bool, optional
1460
+ :param progress_cb: Function for tracking download progress. Total should be equal to len(image_ids) or None.
1461
+ :type progress_cb: tqdm or callable, optional
1462
+ :param progress_cb_type: Type of progress callback. Can be "number" or "size". Default is "number".
1463
+ :type progress_cb_type: str, optional
1464
+ :return: Information about Annotations. See :class:`info_sequence<info_sequence>`
1465
+ :rtype: :class:`List[AnnotationInfo]`
1466
+
1467
+ :Usage example:
1468
+
1469
+ .. code-block:: python
1470
+
1471
+ import supervisely as sly
1472
+
1473
+ os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
1474
+ os.environ['API_TOKEN'] = 'Your Supervisely API Token'
1475
+ api = sly.Api.from_env()
1476
+
1477
+ dataset_id = 254737
1478
+ image_ids = [121236918, 121236919]
1479
+ pbar = tqdm(desc="Download annotations", total=len(image_ids))
1480
+
1481
+ loop = sly.utils.get_or_create_event_loop()
1482
+ ann_infos = loop.run_until_complete(
1483
+ api.annotation.download_batch_async(dataset_id, image_ids, progress_cb=pbar)
1484
+ )
1485
+ """
1486
+
1487
+ # use context to avoid redundant API calls
1488
+ context = self._api.optimization_context
1489
+ context_dataset_id = context.get("dataset_id")
1490
+ project_meta = context.get("project_meta")
1491
+ project_id = context.get("project_id")
1492
+ if dataset_id != context_dataset_id:
1493
+ context["dataset_id"] = dataset_id
1494
+ project_id, project_meta = None, None
1495
+
1496
+ if not isinstance(project_meta, ProjectMeta):
1497
+ if project_id is None:
1498
+ project_id = self._api.dataset.get_info_by_id(dataset_id).project_id
1499
+ context["project_id"] = project_id
1500
+ project_meta = ProjectMeta.from_json(self._api.project.get_meta(project_id))
1501
+ context["project_meta"] = project_meta
1502
+
1503
+ if semaphore is None:
1504
+ semaphore = self._api._get_default_semaphore()
1505
+ tasks = []
1506
+ for image in image_ids:
1507
+ task = self.download_async(
1508
+ image_id=image,
1509
+ semaphore=semaphore,
1510
+ with_custom_data=with_custom_data,
1511
+ force_metadata_for_links=force_metadata_for_links,
1512
+ progress_cb=progress_cb,
1513
+ progress_cb_type=progress_cb_type,
1514
+ )
1515
+ tasks.append(task)
1516
+ ann_infos = await asyncio.gather(*tasks)
1517
+ return ann_infos
supervisely/api/api.py CHANGED
@@ -1248,7 +1248,7 @@ class Api:
1248
1248
  raise ValueError(
1249
1249
  f"Streamed size does not match the expected: {total_streamed} != {expected_size}"
1250
1250
  )
1251
- logger.debug(f"Streamed size: {total_streamed}, expected size: {expected_size}")
1251
+ logger.trace(f"Streamed size: {total_streamed}, expected size: {expected_size}")
1252
1252
  return
1253
1253
  except httpx.RequestError as e:
1254
1254
  retry_range_start = total_streamed + (range_start or 0)
@@ -1478,7 +1478,7 @@ class Api:
1478
1478
  raise ValueError(
1479
1479
  f"Streamed size does not match the expected: {total_streamed} != {expected_size}"
1480
1480
  )
1481
- logger.debug(f"Streamed size: {total_streamed}, expected size: {expected_size}")
1481
+ logger.trace(f"Streamed size: {total_streamed}, expected size: {expected_size}")
1482
1482
  return
1483
1483
  except httpx.RequestError as e:
1484
1484
  retry_range_start = total_streamed + (range_start or 0)
@@ -4,7 +4,17 @@
4
4
  # docs
5
5
  from __future__ import annotations
6
6
 
7
- from typing import Dict, Generator, List, Literal, NamedTuple, Optional, Tuple, Union
7
+ from typing import (
8
+ Any,
9
+ Dict,
10
+ Generator,
11
+ List,
12
+ Literal,
13
+ NamedTuple,
14
+ Optional,
15
+ Tuple,
16
+ Union,
17
+ )
8
18
 
9
19
  from supervisely._utils import abs_url, compress_image_url, is_development
10
20
  from supervisely.api.module_api import (
@@ -61,6 +71,7 @@ class DatasetInfo(NamedTuple):
61
71
  team_id: int
62
72
  workspace_id: int
63
73
  parent_id: Union[int, None]
74
+ custom_data: dict
64
75
 
65
76
  @property
66
77
  def image_preview_url(self):
@@ -135,6 +146,7 @@ class DatasetApi(UpdateableModule, RemoveableModuleApi):
135
146
  ApiField.TEAM_ID,
136
147
  ApiField.WORKSPACE_ID,
137
148
  ApiField.PARENT_ID,
149
+ ApiField.CUSTOM_DATA,
138
150
  ]
139
151
 
140
152
  @staticmethod
@@ -362,6 +374,83 @@ class DatasetApi(UpdateableModule, RemoveableModuleApi):
362
374
  )
363
375
  return dataset_info
364
376
 
377
+ def update(
378
+ self,
379
+ id: int,
380
+ name: Optional[str] = None,
381
+ description: Optional[str] = None,
382
+ custom_data: Optional[Dict[Any, Any]] = None,
383
+ ) -> DatasetInfo:
384
+ """Update Dataset information by given ID.
385
+
386
+ :param id: Dataset ID in Supervisely.
387
+ :type id: int
388
+ :param name: New Dataset name.
389
+ :type name: str, optional
390
+ :param description: New Dataset description.
391
+ :type description: str, optional
392
+ :param custom_data: New custom data.
393
+ :type custom_data: Dict[Any, Any], optional
394
+ :return: Information about Dataset. See :class:`info_sequence<info_sequence>`
395
+ :rtype: :class:`DatasetInfo`
396
+
397
+ :Usage example:
398
+
399
+ .. code-block:: python
400
+
401
+ import supervisely as sly
402
+
403
+ dataset_id = 384126
404
+
405
+ os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
406
+ os.environ['API_TOKEN'] = 'Your Supervisely API Token'
407
+ api = sly.Api.from_env()
408
+
409
+ new_ds = api.dataset.update(dataset_id, name='new_ds', description='new description')
410
+ """
411
+ fields = [name, description, custom_data] # Extend later if needed.
412
+ if all(f is None for f in fields):
413
+ raise ValueError(f"At least one of the fields must be specified: {fields}")
414
+
415
+ payload = {
416
+ ApiField.ID: id,
417
+ ApiField.NAME: name,
418
+ ApiField.DESCRIPTION: description,
419
+ ApiField.CUSTOM_DATA: custom_data,
420
+ }
421
+
422
+ payload = {k: v for k, v in payload.items() if v is not None}
423
+
424
+ response = self._api.post(self._get_update_method(), payload)
425
+ return self._convert_json_info(response.json())
426
+
427
+ def update_custom_data(self, id: int, custom_data: Dict[Any, Any]) -> DatasetInfo:
428
+ """Update custom data for Dataset by given ID.
429
+ Custom data is a dictionary that can store any additional information about the Dataset.
430
+
431
+ :param id: Dataset ID in Supervisely.
432
+ :type id: int
433
+ :param custom_data: New custom data.
434
+ :type custom_data: Dict[Any, Any]
435
+ :return: Information about Dataset. See :class:`info_sequence<info_sequence>`
436
+ :rtype: :class:`DatasetInfo`
437
+
438
+ :Usage example:
439
+
440
+ .. code-block:: python
441
+
442
+ import supervisely as sly
443
+
444
+ dataset_id = 384126
445
+
446
+ os.environ['SERVER_ADDRESS'] = 'https://app.supervisely.com'
447
+ os.environ['API_TOKEN'] = 'Your Supervisely API Token'
448
+ api = sly.Api.from_env()
449
+
450
+ new_ds = api.dataset.update_custom_data(dataset_id, custom_data={'key': 'value'})
451
+ """
452
+ return self.update(id, custom_data=custom_data)
453
+
365
454
  def _get_update_method(self):
366
455
  """ """
367
456
  return "datasets.editInfo"
@@ -6,10 +6,11 @@ from __future__ import annotations
6
6
  import json
7
7
  import re
8
8
  from collections import defaultdict
9
- from typing import Dict, Generator, List, NamedTuple, Optional, Tuple
9
+ from typing import Callable, Dict, Generator, List, NamedTuple, Optional, Tuple, Union
10
10
 
11
11
  import numpy as np
12
12
  from requests_toolbelt import MultipartDecoder, MultipartEncoder
13
+ from tqdm import tqdm
13
14
 
14
15
  from supervisely._utils import batched
15
16
  from supervisely.api.module_api import ApiField, ModuleApi, RemoveableBulkModuleApi
@@ -540,17 +541,25 @@ class FigureApi(RemoveableBulkModuleApi):
540
541
  """
541
542
  return self.download_geometries_batch([figure_id])
542
543
 
543
- def download_geometries_batch(self, ids: List[int]) -> List[dict]:
544
+ def download_geometries_batch(
545
+ self,
546
+ ids: List[int],
547
+ progress_cb: Optional[Union[tqdm, Callable]] = None,
548
+ ) -> List[dict]:
544
549
  """
545
550
  Download figure geometries with given IDs from storage.
546
551
 
547
552
  :param ids: List of figure IDs in Supervisely.
548
553
  :type ids: List[int]
554
+ :param progress_cb: Progress bar to show the download progress. Shows the number of bytes downloaded.
555
+ :type progress_cb: Union[tqdm, Callable], optional
549
556
  :return: List of figure geometries in Supervisely JSON format.
550
557
  :rtype: List[dict]
551
558
  """
552
559
  geometries = {}
553
560
  for idx, part in self._download_geometries_generator(ids):
561
+ if progress_cb is not None:
562
+ progress_cb(len(part.content))
554
563
  geometry_json = json.loads(part.content)
555
564
  geometries[idx] = geometry_json
556
565