supervisely 6.73.227__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 +1 -1
- supervisely/_utils.py +55 -1
- supervisely/api/dataset_api.py +90 -1
- supervisely/api/image_api.py +89 -3
- supervisely/api/project_api.py +285 -1
- supervisely/project/project_type.py +4 -1
- {supervisely-6.73.227.dist-info → supervisely-6.73.228.dist-info}/METADATA +1 -1
- {supervisely-6.73.227.dist-info → supervisely-6.73.228.dist-info}/RECORD +12 -12
- {supervisely-6.73.227.dist-info → supervisely-6.73.228.dist-info}/LICENSE +0 -0
- {supervisely-6.73.227.dist-info → supervisely-6.73.228.dist-info}/WHEEL +0 -0
- {supervisely-6.73.227.dist-info → supervisely-6.73.228.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.227.dist-info → supervisely-6.73.228.dist-info}/top_level.txt +0 -0
supervisely/__init__.py
CHANGED
|
@@ -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.
|
|
312
|
+
os.environ["MINIMUM_INSTANCE_VERSION_FOR_SDK"] = "6.12.5"
|
supervisely/_utils.py
CHANGED
|
@@ -14,7 +14,7 @@ import urllib
|
|
|
14
14
|
from datetime import datetime
|
|
15
15
|
from functools import wraps
|
|
16
16
|
from tempfile import gettempdir
|
|
17
|
-
from typing import List, Literal, Optional
|
|
17
|
+
from typing import Any, Dict, List, Literal, Optional, Tuple
|
|
18
18
|
|
|
19
19
|
import numpy as np
|
|
20
20
|
from requests.utils import DEFAULT_CA_BUNDLE_PATH
|
|
@@ -316,6 +316,15 @@ def get_readable_datetime(value: str) -> str:
|
|
|
316
316
|
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
317
317
|
|
|
318
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
|
+
|
|
319
328
|
def get_certificates_list(path: str = DEFAULT_CA_BUNDLE_PATH) -> List[str]:
|
|
320
329
|
with open(path, "r", encoding="ascii") as f:
|
|
321
330
|
content = f.read().strip()
|
|
@@ -385,6 +394,51 @@ def add_callback(func, callback):
|
|
|
385
394
|
return wrapper
|
|
386
395
|
|
|
387
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
|
+
|
|
388
442
|
def get_or_create_event_loop() -> asyncio.AbstractEventLoop:
|
|
389
443
|
"""
|
|
390
444
|
Get the current event loop or create a new one if it doesn't exist.
|
supervisely/api/dataset_api.py
CHANGED
|
@@ -4,7 +4,17 @@
|
|
|
4
4
|
# docs
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
-
from typing import
|
|
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"
|
supervisely/api/image_api.py
CHANGED
|
@@ -40,6 +40,7 @@ from tqdm import tqdm
|
|
|
40
40
|
|
|
41
41
|
from supervisely._utils import (
|
|
42
42
|
batched,
|
|
43
|
+
compare_dicts,
|
|
43
44
|
generate_free_name,
|
|
44
45
|
get_bytes_hash,
|
|
45
46
|
resize_image_url,
|
|
@@ -1135,7 +1136,14 @@ class ImageApi(RemoveableBulkModuleApi):
|
|
|
1135
1136
|
)
|
|
1136
1137
|
|
|
1137
1138
|
def upload_path(
|
|
1138
|
-
self,
|
|
1139
|
+
self,
|
|
1140
|
+
dataset_id: int,
|
|
1141
|
+
name: str,
|
|
1142
|
+
path: str,
|
|
1143
|
+
meta: Optional[Dict] = None,
|
|
1144
|
+
validate_meta: Optional[bool] = False,
|
|
1145
|
+
use_strict_validation: Optional[bool] = False,
|
|
1146
|
+
use_caching_for_validation: Optional[bool] = False,
|
|
1139
1147
|
) -> ImageInfo:
|
|
1140
1148
|
"""
|
|
1141
1149
|
Uploads Image with given name from given local path to Dataset.
|
|
@@ -1148,6 +1156,12 @@ class ImageApi(RemoveableBulkModuleApi):
|
|
|
1148
1156
|
:type path: str
|
|
1149
1157
|
:param meta: Image metadata.
|
|
1150
1158
|
:type meta: dict, optional
|
|
1159
|
+
:param validate_meta: If True, validates provided meta with saved JSON schema.
|
|
1160
|
+
:type validate_meta: bool, optional
|
|
1161
|
+
:param use_strict_validation: If True, uses strict validation.
|
|
1162
|
+
:type use_strict_validation: bool, optional
|
|
1163
|
+
:param use_caching_for_validation: If True, uses caching for validation.
|
|
1164
|
+
:type use_caching_for_validation: bool, optional
|
|
1151
1165
|
:return: Information about Image. See :class:`info_sequence<info_sequence>`
|
|
1152
1166
|
:rtype: :class:`ImageInfo`
|
|
1153
1167
|
:Usage example:
|
|
@@ -1163,7 +1177,15 @@ class ImageApi(RemoveableBulkModuleApi):
|
|
|
1163
1177
|
img_info = api.image.upload_path(dataset_id, name="7777.jpeg", path="/home/admin/Downloads/7777.jpeg")
|
|
1164
1178
|
"""
|
|
1165
1179
|
metas = None if meta is None else [meta]
|
|
1166
|
-
return self.upload_paths(
|
|
1180
|
+
return self.upload_paths(
|
|
1181
|
+
dataset_id,
|
|
1182
|
+
[name],
|
|
1183
|
+
[path],
|
|
1184
|
+
metas=metas,
|
|
1185
|
+
validate_meta=validate_meta,
|
|
1186
|
+
use_strict_validation=use_strict_validation,
|
|
1187
|
+
use_caching_for_validation=use_caching_for_validation,
|
|
1188
|
+
)[0]
|
|
1167
1189
|
|
|
1168
1190
|
def upload_paths(
|
|
1169
1191
|
self,
|
|
@@ -1173,6 +1195,9 @@ class ImageApi(RemoveableBulkModuleApi):
|
|
|
1173
1195
|
progress_cb: Optional[Union[tqdm, Callable]] = None,
|
|
1174
1196
|
metas: Optional[List[Dict]] = None,
|
|
1175
1197
|
conflict_resolution: Optional[Literal["rename", "skip", "replace"]] = None,
|
|
1198
|
+
validate_meta: Optional[bool] = False,
|
|
1199
|
+
use_strict_validation: Optional[bool] = False,
|
|
1200
|
+
use_caching_for_validation: Optional[bool] = False,
|
|
1176
1201
|
) -> List[ImageInfo]:
|
|
1177
1202
|
"""
|
|
1178
1203
|
Uploads Images with given names from given local path to Dataset.
|
|
@@ -1189,6 +1214,12 @@ class ImageApi(RemoveableBulkModuleApi):
|
|
|
1189
1214
|
:type metas: List[dict], optional
|
|
1190
1215
|
:param conflict_resolution: The strategy to resolve upload conflicts. 'Replace' option will replace the existing images in the dataset with the new images. The images that are being deleted are logged. 'Skip' option will ignore the upload of new images that would result in a conflict. An original image's ImageInfo list will be returned instead. 'Rename' option will rename the new images to prevent any conflict.
|
|
1191
1216
|
:type conflict_resolution: Optional[Literal["rename", "skip", "replace"]]
|
|
1217
|
+
:param validate_meta: If True, validates provided meta with saved JSON schema.
|
|
1218
|
+
:type validate_meta: bool, optional
|
|
1219
|
+
:param use_strict_validation: If True, uses strict validation.
|
|
1220
|
+
:type use_strict_validation: bool, optional
|
|
1221
|
+
:param use_caching_for_validation: If True, uses caching for validation.
|
|
1222
|
+
:type use_caching_for_validation: bool, optional
|
|
1192
1223
|
:raises: :class:`ValueError` if len(names) != len(paths)
|
|
1193
1224
|
:return: List with information about Images. See :class:`info_sequence<info_sequence>`
|
|
1194
1225
|
:rtype: :class:`List[ImageInfo]`
|
|
@@ -1214,7 +1245,14 @@ class ImageApi(RemoveableBulkModuleApi):
|
|
|
1214
1245
|
self._upload_data_bulk(path_to_bytes_stream, zip(paths, hashes), progress_cb=progress_cb)
|
|
1215
1246
|
|
|
1216
1247
|
return self.upload_hashes(
|
|
1217
|
-
dataset_id,
|
|
1248
|
+
dataset_id,
|
|
1249
|
+
names,
|
|
1250
|
+
hashes,
|
|
1251
|
+
metas=metas,
|
|
1252
|
+
conflict_resolution=conflict_resolution,
|
|
1253
|
+
validate_meta=validate_meta,
|
|
1254
|
+
use_strict_validation=use_strict_validation,
|
|
1255
|
+
use_caching_for_validation=use_caching_for_validation,
|
|
1218
1256
|
)
|
|
1219
1257
|
|
|
1220
1258
|
def upload_np(
|
|
@@ -1492,6 +1530,9 @@ class ImageApi(RemoveableBulkModuleApi):
|
|
|
1492
1530
|
batch_size: Optional[int] = 50,
|
|
1493
1531
|
skip_validation: Optional[bool] = False,
|
|
1494
1532
|
conflict_resolution: Optional[Literal["rename", "skip", "replace"]] = None,
|
|
1533
|
+
validate_meta: Optional[bool] = False,
|
|
1534
|
+
use_strict_validation: Optional[bool] = False,
|
|
1535
|
+
use_caching_for_validation: Optional[bool] = False,
|
|
1495
1536
|
) -> List[ImageInfo]:
|
|
1496
1537
|
"""
|
|
1497
1538
|
Upload images from given hashes to Dataset.
|
|
@@ -1512,6 +1553,12 @@ class ImageApi(RemoveableBulkModuleApi):
|
|
|
1512
1553
|
:type skip_validation: bool, optional
|
|
1513
1554
|
:param conflict_resolution: The strategy to resolve upload conflicts. 'Replace' option will replace the existing images in the dataset with the new images. The images that are being deleted are logged. 'Skip' option will ignore the upload of new images that would result in a conflict. An original image's ImageInfo list will be returned instead. 'Rename' option will rename the new images to prevent any conflict.
|
|
1514
1555
|
:type conflict_resolution: Optional[Literal["rename", "skip", "replace"]]
|
|
1556
|
+
:param validate_meta: If True, validates provided meta with saved JSON schema.
|
|
1557
|
+
:type validate_meta: bool, optional
|
|
1558
|
+
:param use_strict_validation: If True, uses strict validation.
|
|
1559
|
+
:type use_strict_validation: bool, optional
|
|
1560
|
+
:param use_caching_for_validation: If True, uses caching for validation.
|
|
1561
|
+
:type use_caching_for_validation: bool, optional
|
|
1515
1562
|
:return: List with information about Images. See :class:`info_sequence<info_sequence>`
|
|
1516
1563
|
:rtype: :class:`List[ImageInfo]`
|
|
1517
1564
|
:Usage example:
|
|
@@ -1553,6 +1600,9 @@ class ImageApi(RemoveableBulkModuleApi):
|
|
|
1553
1600
|
batch_size=batch_size,
|
|
1554
1601
|
skip_validation=skip_validation,
|
|
1555
1602
|
conflict_resolution=conflict_resolution,
|
|
1603
|
+
validate_meta=validate_meta,
|
|
1604
|
+
use_strict_validation=use_strict_validation,
|
|
1605
|
+
use_caching_for_validation=use_caching_for_validation,
|
|
1556
1606
|
)
|
|
1557
1607
|
|
|
1558
1608
|
def upload_id(
|
|
@@ -1750,8 +1800,44 @@ class ImageApi(RemoveableBulkModuleApi):
|
|
|
1750
1800
|
force_metadata_for_links=True,
|
|
1751
1801
|
skip_validation=False,
|
|
1752
1802
|
conflict_resolution: Optional[Literal["rename", "skip", "replace"]] = None,
|
|
1803
|
+
validate_meta: Optional[bool] = False,
|
|
1804
|
+
use_strict_validation: Optional[bool] = False,
|
|
1805
|
+
use_caching_for_validation: Optional[bool] = False,
|
|
1753
1806
|
):
|
|
1754
1807
|
""" """
|
|
1808
|
+
if use_strict_validation and not validate_meta:
|
|
1809
|
+
raise ValueError(
|
|
1810
|
+
"use_strict_validation is set to True, while validate_meta is set to False. "
|
|
1811
|
+
"Please set validate_meta to True to use strict validation "
|
|
1812
|
+
"or disable strict validation by setting use_strict_validation to False."
|
|
1813
|
+
)
|
|
1814
|
+
if validate_meta:
|
|
1815
|
+
dataset_info = self._api.dataset.get_info_by_id(dataset_id)
|
|
1816
|
+
|
|
1817
|
+
validation_schema = self._api.project.get_validation_schema(
|
|
1818
|
+
dataset_info.project_id, use_caching=use_caching_for_validation
|
|
1819
|
+
)
|
|
1820
|
+
|
|
1821
|
+
if validation_schema is None:
|
|
1822
|
+
raise ValueError(
|
|
1823
|
+
"Validation schema is not set for the project, while "
|
|
1824
|
+
"validate_meta is set to True. Either disable the validation "
|
|
1825
|
+
"or set the validation schema for the project using the "
|
|
1826
|
+
"api.project.set_validation_schema method."
|
|
1827
|
+
)
|
|
1828
|
+
|
|
1829
|
+
for idx, meta in enumerate(metas):
|
|
1830
|
+
missing_fields, extra_fields = compare_dicts(
|
|
1831
|
+
validation_schema, meta, strict=use_strict_validation
|
|
1832
|
+
)
|
|
1833
|
+
|
|
1834
|
+
if missing_fields or extra_fields:
|
|
1835
|
+
raise ValueError(
|
|
1836
|
+
f"Validation failed for the metadata of the image with index {idx} and name {names[idx]}. "
|
|
1837
|
+
"Please check the metadata and try again. "
|
|
1838
|
+
f"Missing fields: {missing_fields}, Extra fields: {extra_fields}"
|
|
1839
|
+
)
|
|
1840
|
+
|
|
1755
1841
|
if (
|
|
1756
1842
|
conflict_resolution is not None
|
|
1757
1843
|
and conflict_resolution not in SUPPORTED_CONFLICT_RESOLUTIONS
|
supervisely/api/project_api.py
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
# docs
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
+
import os
|
|
7
8
|
from collections import defaultdict
|
|
9
|
+
from copy import deepcopy
|
|
8
10
|
from typing import (
|
|
9
11
|
TYPE_CHECKING,
|
|
10
12
|
Any,
|
|
@@ -25,7 +27,13 @@ if TYPE_CHECKING:
|
|
|
25
27
|
from datetime import datetime, timedelta
|
|
26
28
|
|
|
27
29
|
from supervisely import logger
|
|
28
|
-
from supervisely._utils import
|
|
30
|
+
from supervisely._utils import (
|
|
31
|
+
abs_url,
|
|
32
|
+
compare_dicts,
|
|
33
|
+
compress_image_url,
|
|
34
|
+
get_unix_timestamp,
|
|
35
|
+
is_development,
|
|
36
|
+
)
|
|
29
37
|
from supervisely.annotation.annotation import TagCollection
|
|
30
38
|
from supervisely.annotation.obj_class import ObjClass
|
|
31
39
|
from supervisely.annotation.obj_class_collection import ObjClassCollection
|
|
@@ -36,6 +44,7 @@ from supervisely.api.module_api import (
|
|
|
36
44
|
RemoveableModuleApi,
|
|
37
45
|
UpdateableModule,
|
|
38
46
|
)
|
|
47
|
+
from supervisely.io.json import dump_json_file, load_json_file
|
|
39
48
|
from supervisely.project.project_meta import ProjectMeta
|
|
40
49
|
from supervisely.project.project_meta import ProjectMetaJsonFields as MetaJsonF
|
|
41
50
|
from supervisely.project.project_settings import (
|
|
@@ -43,6 +52,9 @@ from supervisely.project.project_settings import (
|
|
|
43
52
|
ProjectSettingsJsonFields,
|
|
44
53
|
)
|
|
45
54
|
from supervisely.project.project_type import (
|
|
55
|
+
_METADATA_SYSTEM_KEY,
|
|
56
|
+
_METADATA_TIMESTAMP_KEY,
|
|
57
|
+
_METADATA_VALIDATION_SCHEMA_KEY,
|
|
46
58
|
_MULTISPECTRAL_TAG_NAME,
|
|
47
59
|
_MULTIVIEW_TAG_NAME,
|
|
48
60
|
ProjectType,
|
|
@@ -969,6 +981,278 @@ class ProjectApi(CloneableModuleApi, UpdateableModule, RemoveableModuleApi):
|
|
|
969
981
|
)
|
|
970
982
|
return response.json()
|
|
971
983
|
|
|
984
|
+
def get_custom_data(self, id: int) -> Dict[Any, Any]:
|
|
985
|
+
"""Returns custom data of the Project by ID.
|
|
986
|
+
Custom data is a dictionary that can be used to store any additional information.
|
|
987
|
+
|
|
988
|
+
:param id: Project ID in Supervisely.
|
|
989
|
+
:type id: int
|
|
990
|
+
:return: Custom data of the Project
|
|
991
|
+
:rtype: :class:`dict`
|
|
992
|
+
|
|
993
|
+
:Usage example:
|
|
994
|
+
|
|
995
|
+
.. code-block:: python
|
|
996
|
+
|
|
997
|
+
import supervisely as sly
|
|
998
|
+
|
|
999
|
+
api = sly.Api.from_env()
|
|
1000
|
+
|
|
1001
|
+
project_id = 123456
|
|
1002
|
+
|
|
1003
|
+
custom_data = api.project.get_custom_data(project_id)
|
|
1004
|
+
|
|
1005
|
+
print(custom_data) # Output: {'key': 'value'}
|
|
1006
|
+
"""
|
|
1007
|
+
return self.get_info_by_id(id).custom_data
|
|
1008
|
+
|
|
1009
|
+
def _get_system_custom_data(self, id: int) -> Dict[Any, Any]:
|
|
1010
|
+
"""Returns system custom data of the Project by ID.
|
|
1011
|
+
System custom data is just a part of custom data that is used to store system information
|
|
1012
|
+
and obtained by the key `_METADATA_SYSTEM_KEY`.
|
|
1013
|
+
|
|
1014
|
+
:param id: Project ID in Supervisely.
|
|
1015
|
+
:type id: int
|
|
1016
|
+
:return: System custom data of the Project
|
|
1017
|
+
:rtype: :class:`dict`
|
|
1018
|
+
|
|
1019
|
+
:Usage example:
|
|
1020
|
+
|
|
1021
|
+
.. code-block:: python
|
|
1022
|
+
|
|
1023
|
+
import supervisely as sly
|
|
1024
|
+
|
|
1025
|
+
api = sly.Api.from_env()
|
|
1026
|
+
|
|
1027
|
+
project_id = 123456
|
|
1028
|
+
|
|
1029
|
+
system_custom_data = api.project._get_system_custom_data(project_id)
|
|
1030
|
+
|
|
1031
|
+
print(system_custom_data)
|
|
1032
|
+
"""
|
|
1033
|
+
return self.get_info_by_id(id).custom_data.get(_METADATA_SYSTEM_KEY, {})
|
|
1034
|
+
|
|
1035
|
+
def get_validation_schema(self, id: int, use_caching: bool = False) -> Optional[Dict[Any, Any]]:
|
|
1036
|
+
"""Returns validation schema of the Project by ID.
|
|
1037
|
+
Validation schema is a dictionary that can be used to validate metadata of each entity in the project
|
|
1038
|
+
if corresnpoding schema is provided.
|
|
1039
|
+
If using caching, the schema will be loaded from the cache if available.
|
|
1040
|
+
Use cached version only in scenarios when the schema is not expected to change,
|
|
1041
|
+
otherwise it may lead to checks with outdated schema.
|
|
1042
|
+
|
|
1043
|
+
:param id: Project ID in Supervisely.
|
|
1044
|
+
:type id: int
|
|
1045
|
+
:param use_caching: If True, uses cached version of the schema if available.
|
|
1046
|
+
NOTE: This may lead to checks with outdated schema. Use with caution.
|
|
1047
|
+
And only in scenarios when the schema is not expected to change.
|
|
1048
|
+
:return: Validation schema of the Project
|
|
1049
|
+
:rtype: :class:`dict`
|
|
1050
|
+
|
|
1051
|
+
:Usage example:
|
|
1052
|
+
|
|
1053
|
+
.. code-block:: python
|
|
1054
|
+
|
|
1055
|
+
import supervisely as sly
|
|
1056
|
+
|
|
1057
|
+
api = sly.Api.from_env()
|
|
1058
|
+
|
|
1059
|
+
project_id = 123456
|
|
1060
|
+
|
|
1061
|
+
validation_schema = api.project.get_validation_schema(project_id)
|
|
1062
|
+
|
|
1063
|
+
print(validation_schema) # Output: {'key': 'Description of the field'}
|
|
1064
|
+
"""
|
|
1065
|
+
SCHEMA_DIFF_THRESHOLD = 60 * 60 # 1 hour
|
|
1066
|
+
json_cache_filename = os.path.join(os.getcwd(), f"{id}_validation_schema.json")
|
|
1067
|
+
|
|
1068
|
+
if use_caching:
|
|
1069
|
+
if os.path.isfile(json_cache_filename):
|
|
1070
|
+
try:
|
|
1071
|
+
schema = load_json_file(json_cache_filename)
|
|
1072
|
+
timestamp = schema.pop(_METADATA_TIMESTAMP_KEY, 0)
|
|
1073
|
+
|
|
1074
|
+
if get_unix_timestamp() - timestamp < SCHEMA_DIFF_THRESHOLD:
|
|
1075
|
+
return schema
|
|
1076
|
+
except RuntimeError:
|
|
1077
|
+
pass
|
|
1078
|
+
|
|
1079
|
+
schema = self._get_system_custom_data(id).get(_METADATA_VALIDATION_SCHEMA_KEY)
|
|
1080
|
+
if schema and use_caching:
|
|
1081
|
+
schema_with_timestamp = deepcopy(schema)
|
|
1082
|
+
schema_with_timestamp[_METADATA_TIMESTAMP_KEY] = get_unix_timestamp()
|
|
1083
|
+
dump_json_file(schema_with_timestamp, json_cache_filename)
|
|
1084
|
+
|
|
1085
|
+
return schema
|
|
1086
|
+
|
|
1087
|
+
def _edit_validation_schema(
|
|
1088
|
+
self, id: int, schema: Optional[Dict[Any, Any]] = None
|
|
1089
|
+
) -> Dict[Any, Any]:
|
|
1090
|
+
"""Edits validation schema of the Project by ID.
|
|
1091
|
+
Do not use this method directly, use `set_validation_schema` or `remove_validation_schema` instead.
|
|
1092
|
+
|
|
1093
|
+
:param id: Project ID in Supervisely.
|
|
1094
|
+
:type id: int
|
|
1095
|
+
:param schema: Validation schema to set. If None, removes validation schema.
|
|
1096
|
+
:type schema: dict, optional
|
|
1097
|
+
:return: Project information in dict format
|
|
1098
|
+
:rtype: :class:`dict`
|
|
1099
|
+
|
|
1100
|
+
:Usage example:
|
|
1101
|
+
|
|
1102
|
+
.. code-block:: python
|
|
1103
|
+
|
|
1104
|
+
import supervisely as sly
|
|
1105
|
+
|
|
1106
|
+
api = sly.Api.from_env()
|
|
1107
|
+
|
|
1108
|
+
project_id = 123456
|
|
1109
|
+
|
|
1110
|
+
schema = {'key': 'Description of the field'}
|
|
1111
|
+
|
|
1112
|
+
api.project._edit_validation_schema(project_id, schema) #Set new validation schema.
|
|
1113
|
+
api.project._edit_validation_schema(project_id) #Remove validation schema.
|
|
1114
|
+
"""
|
|
1115
|
+
custom_data = self.get_custom_data(id)
|
|
1116
|
+
system_data = custom_data.setdefault(_METADATA_SYSTEM_KEY, {})
|
|
1117
|
+
|
|
1118
|
+
if not schema:
|
|
1119
|
+
system_data.pop(_METADATA_VALIDATION_SCHEMA_KEY, None)
|
|
1120
|
+
else:
|
|
1121
|
+
system_data[_METADATA_VALIDATION_SCHEMA_KEY] = schema
|
|
1122
|
+
return self.update_custom_data(id, custom_data)
|
|
1123
|
+
|
|
1124
|
+
def set_validation_schema(self, id: int, schema: Dict[Any, Any]) -> Dict[Any, Any]:
|
|
1125
|
+
"""Sets validation schema of the Project by ID.
|
|
1126
|
+
NOTE: This method will overwrite existing validation schema. To extend existing schema,
|
|
1127
|
+
use `get_validation_schema` first to get current schema, then update it and use this method to set new schema.
|
|
1128
|
+
|
|
1129
|
+
:param id: Project ID in Supervisely.
|
|
1130
|
+
:type id: int
|
|
1131
|
+
:param schema: Validation schema to set.
|
|
1132
|
+
:type schema: dict
|
|
1133
|
+
:return: Project information in dict format
|
|
1134
|
+
:rtype: :class:`dict`
|
|
1135
|
+
|
|
1136
|
+
:Usage example:
|
|
1137
|
+
|
|
1138
|
+
.. code-block:: python
|
|
1139
|
+
|
|
1140
|
+
import supervisely as sly
|
|
1141
|
+
|
|
1142
|
+
api = sly.Api.from_env()
|
|
1143
|
+
|
|
1144
|
+
project_id = 123456
|
|
1145
|
+
|
|
1146
|
+
schema = {'key': 'Description of the field'}
|
|
1147
|
+
|
|
1148
|
+
api.project.set_validation_schema(project_id, schema)
|
|
1149
|
+
"""
|
|
1150
|
+
return self._edit_validation_schema(id, schema)
|
|
1151
|
+
|
|
1152
|
+
def remove_validation_schema(self, id: int) -> Dict[Any, Any]:
|
|
1153
|
+
"""Removes validation schema of the Project by ID.
|
|
1154
|
+
|
|
1155
|
+
:param id: Project ID in Supervisely.
|
|
1156
|
+
:type id: int
|
|
1157
|
+
:return: Project information in dict format
|
|
1158
|
+
:rtype: :class:`dict`
|
|
1159
|
+
|
|
1160
|
+
:Usage example:
|
|
1161
|
+
|
|
1162
|
+
.. code-block:: python
|
|
1163
|
+
|
|
1164
|
+
import supervisely as sly
|
|
1165
|
+
|
|
1166
|
+
api = sly.Api.from_env()
|
|
1167
|
+
|
|
1168
|
+
project_id = 123456
|
|
1169
|
+
|
|
1170
|
+
api.project.remove_validation_schema(project_id)
|
|
1171
|
+
"""
|
|
1172
|
+
return self._edit_validation_schema(id)
|
|
1173
|
+
|
|
1174
|
+
def validate_entities_schema(
|
|
1175
|
+
self, id: int, strict: bool = False
|
|
1176
|
+
) -> List[Dict[str, Union[id, str, List[str], List[Any]]]]:
|
|
1177
|
+
"""Validates entities of the Project by ID using validation schema.
|
|
1178
|
+
Returns list of entities that do not match the schema.
|
|
1179
|
+
|
|
1180
|
+
Example of the returned list:
|
|
1181
|
+
|
|
1182
|
+
[
|
|
1183
|
+
{
|
|
1184
|
+
"entity_id": 123456,
|
|
1185
|
+
"entity_name": "image.jpg",
|
|
1186
|
+
"missing_fields": ["location"],
|
|
1187
|
+
"extra_fields": ["city.name"] <- Nested field (field "name" of the field "city")
|
|
1188
|
+
}
|
|
1189
|
+
]
|
|
1190
|
+
|
|
1191
|
+
:param id: Project ID in Supervisely.
|
|
1192
|
+
:type id: int
|
|
1193
|
+
:param strict: If strict is disabled, only checks if the entity has all the fields from the schema.
|
|
1194
|
+
Any extra fields in the entity will be ignored and will not be considered as an error.
|
|
1195
|
+
If strict is enabled, checks that the entity custom data is an exact match to the schema.
|
|
1196
|
+
:type strict: bool, optional
|
|
1197
|
+
:return: List of dictionaries with information about entities that do not match the schema.
|
|
1198
|
+
:rtype: :class:`List[Dict[str, Union[id, str, List[str], List[Any]]]`
|
|
1199
|
+
|
|
1200
|
+
:Usage example:
|
|
1201
|
+
|
|
1202
|
+
.. code-block:: python
|
|
1203
|
+
|
|
1204
|
+
import supervisely as sly
|
|
1205
|
+
|
|
1206
|
+
api = sly.Api.from_env()
|
|
1207
|
+
|
|
1208
|
+
project_id = 123456
|
|
1209
|
+
|
|
1210
|
+
incorrect_entities = api.project.validate_entities_schema(project_id)
|
|
1211
|
+
|
|
1212
|
+
for entity in incorrect_entities:
|
|
1213
|
+
print(entity.id, entity.name) # Output: 123456, 'image.jpg'
|
|
1214
|
+
"""
|
|
1215
|
+
validation_schema = self.get_validation_schema(id)
|
|
1216
|
+
if not validation_schema:
|
|
1217
|
+
raise ValueError("Validation schema is not set for this project.")
|
|
1218
|
+
|
|
1219
|
+
info = self.get_info_by_id(id)
|
|
1220
|
+
listing_method = {
|
|
1221
|
+
# Mapping of project type to listing method.
|
|
1222
|
+
ProjectType.IMAGES.value: self._api.image.get_list,
|
|
1223
|
+
}
|
|
1224
|
+
custom_data_properties = {
|
|
1225
|
+
# Mapping of project type to custom data property name.
|
|
1226
|
+
# Can be different for different project types.
|
|
1227
|
+
ProjectType.IMAGES.value: "meta"
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
listing_method = listing_method.get(info.type)
|
|
1231
|
+
custom_data_property = custom_data_properties.get(info.type)
|
|
1232
|
+
|
|
1233
|
+
if not listing_method or not custom_data_property:
|
|
1234
|
+
# TODO: Add support for other project types.
|
|
1235
|
+
raise NotImplementedError("Validation schema is not supported for this project type.")
|
|
1236
|
+
|
|
1237
|
+
entities = listing_method(project_id=id)
|
|
1238
|
+
incorrect_entities = []
|
|
1239
|
+
|
|
1240
|
+
for entity in entities:
|
|
1241
|
+
custom_data = getattr(entity, custom_data_property)
|
|
1242
|
+
missing_fields, extra_fields = compare_dicts(
|
|
1243
|
+
validation_schema, custom_data, strict=strict
|
|
1244
|
+
)
|
|
1245
|
+
if missing_fields or extra_fields:
|
|
1246
|
+
entry = {
|
|
1247
|
+
"entity_id": entity.id,
|
|
1248
|
+
"entity_name": entity.name,
|
|
1249
|
+
"missing_fields": missing_fields,
|
|
1250
|
+
"extra_fields": extra_fields,
|
|
1251
|
+
}
|
|
1252
|
+
incorrect_entities.append(entry)
|
|
1253
|
+
|
|
1254
|
+
return incorrect_entities
|
|
1255
|
+
|
|
972
1256
|
def get_settings(self, id: int) -> Dict[str, str]:
|
|
973
1257
|
info = self._get_info_by_id(id, "projects.info")
|
|
974
1258
|
return info.settings
|
|
@@ -11,7 +11,10 @@ class ProjectType(StrEnum):
|
|
|
11
11
|
POINT_CLOUD_EPISODES = "point_cloud_episodes"
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
|
|
15
14
|
# Constants for multispectral and multiview projects.
|
|
16
15
|
_MULTISPECTRAL_TAG_NAME = "multispectral"
|
|
17
16
|
_MULTIVIEW_TAG_NAME = "multiview"
|
|
17
|
+
|
|
18
|
+
_METADATA_SYSTEM_KEY = "sly_system"
|
|
19
|
+
_METADATA_VALIDATION_SCHEMA_KEY = "validation_schema"
|
|
20
|
+
_METADATA_TIMESTAMP_KEY = "validation_schema_timestamp"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
supervisely/README.md,sha256=XM-DiMC6To3I9RjQZ0c61905EFRR_jnCUx2q3uNR-X8,3331
|
|
2
|
-
supervisely/__init__.py,sha256=
|
|
3
|
-
supervisely/_utils.py,sha256=
|
|
2
|
+
supervisely/__init__.py,sha256=Ghm5UsSQGjTr7cWetpSxewX0Rys9yC3rb3PQJQJD0_E,10799
|
|
3
|
+
supervisely/_utils.py,sha256=2l9e8L7-p9twlwsBTFKZPM71bCU0aeP3rvs31wy0P-A,15059
|
|
4
4
|
supervisely/function_wrapper.py,sha256=R5YajTQ0GnRp2vtjwfC9hINkzQc0JiyGsu8TER373xY,1912
|
|
5
5
|
supervisely/sly_logger.py,sha256=LG1wTyyctyEKuCuKM2IKf_SMPH7BzkTsFdO-0tnorzg,6225
|
|
6
6
|
supervisely/tiny_timer.py,sha256=hkpe_7FE6bsKL79blSs7WBaktuPavEVu67IpEPrfmjE,183
|
|
@@ -24,11 +24,11 @@ supervisely/api/agent_api.py,sha256=ShWAIlXcWXcyI9fqVuP5GZVCigCMJmjnvdGUfLspD6Y,
|
|
|
24
24
|
supervisely/api/annotation_api.py,sha256=kgbxlAmvwLX7nMFnQ23hZDZryD3tVSehfqRkLEGXQXA,58828
|
|
25
25
|
supervisely/api/api.py,sha256=BdN99Znu_nLjnt--i6xoN9W4v0Am3kYlkqZZeIy4UuE,60633
|
|
26
26
|
supervisely/api/app_api.py,sha256=hjoL24Nc1POlIqz5bkpMBij5D-cwCHvkznkaGpODyzA,67086
|
|
27
|
-
supervisely/api/dataset_api.py,sha256=
|
|
27
|
+
supervisely/api/dataset_api.py,sha256=2-SQBlgEnIN-0uvDbtPlSXr6ztBeZ3WPryhkOtpBmk4,40786
|
|
28
28
|
supervisely/api/file_api.py,sha256=RM3clcdcfj0MtSqGdu6afCqUieYj_2q5gcEch_36tCE,82421
|
|
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=
|
|
31
|
+
supervisely/api/image_api.py,sha256=BMhlJZwfbxFx8eHBQaKNmGI9a7UbOpJrIEmC6Eci-Ck,162395
|
|
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
|
|
@@ -36,7 +36,7 @@ supervisely/api/module_api.py,sha256=nFMvROP7XB6_BVSsP9W_eEKiTlcGYxezyMCV4pdqk8k
|
|
|
36
36
|
supervisely/api/neural_network_api.py,sha256=ktPVRO4Jeulougio8F0mioJJHwRJcX250Djp1wBoQ9c,7620
|
|
37
37
|
supervisely/api/object_class_api.py,sha256=-rQcKwhBw3iL9KNH9c1ROgoimgWM1ls6Wi_tb1R-MzY,7683
|
|
38
38
|
supervisely/api/plugin_api.py,sha256=TlfrosdRuYG4NUxk92QiQoVaOdztFspPpygyVa3M3zk,5283
|
|
39
|
-
supervisely/api/project_api.py,sha256=
|
|
39
|
+
supervisely/api/project_api.py,sha256=3_4wP3RqLHhbdftwu2LF2iKUMmFxrhk3FryL-OpplZo,78633
|
|
40
40
|
supervisely/api/project_class_api.py,sha256=5cyjdGPPb2tpttu5WmYoOxUNiDxqiojschkhZumF0KM,1426
|
|
41
41
|
supervisely/api/remote_storage_api.py,sha256=xy9-j5hSftVcAILyqF_mQdQ1DUywt9msq2QYuSE-PVY,15032
|
|
42
42
|
supervisely/api/report_api.py,sha256=Om7CGulUbQ4BuJ16eDtz7luLe0JQNqab-LoLpUXu7YE,7123
|
|
@@ -938,7 +938,7 @@ supervisely/project/pointcloud_project.py,sha256=Y8Xhi6Hg-KyztwFncezuDfKTt2FILss
|
|
|
938
938
|
supervisely/project/project.py,sha256=8ghaq9OCFRdhTNuKo_morBgVxgFOS1KKpmU8Kyl1vdo,180328
|
|
939
939
|
supervisely/project/project_meta.py,sha256=26s8IiHC5Pg8B1AQi6_CrsWteioJP2in00cRNe8QlW0,51423
|
|
940
940
|
supervisely/project/project_settings.py,sha256=NLThzU_DCynOK6hkHhVdFyezwprn9UqlnrLDe_3qhkY,9347
|
|
941
|
-
supervisely/project/project_type.py,sha256=
|
|
941
|
+
supervisely/project/project_type.py,sha256=_3RqW2CnDBKFOvSIrQT1RJQaiHirs34_jiQS8CkwCpo,530
|
|
942
942
|
supervisely/project/readme_template.md,sha256=rGmSLRVUSGjvorjpzl0sZ7YA4sKfDexl95NFtMISj3I,9128
|
|
943
943
|
supervisely/project/upload.py,sha256=AjgHYgVZwUE25ygC5pqvFjdAladbyB8T78mlet5Qpho,3750
|
|
944
944
|
supervisely/project/video_project.py,sha256=AoZDIF3TAPke3WVZAOSWnuyIs12sFZsY5CxARSNQ5N8,63740
|
|
@@ -997,9 +997,9 @@ supervisely/worker_proto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
|
|
|
997
997
|
supervisely/worker_proto/worker_api_pb2.py,sha256=VQfi5JRBHs2pFCK1snec3JECgGnua3Xjqw_-b3aFxuM,59142
|
|
998
998
|
supervisely/worker_proto/worker_api_pb2_grpc.py,sha256=3BwQXOaP9qpdi0Dt9EKG--Lm8KGN0C5AgmUfRv77_Jk,28940
|
|
999
999
|
supervisely_lib/__init__.py,sha256=7-3QnN8Zf0wj8NCr2oJmqoQWMKKPKTECvjH9pd2S5vY,159
|
|
1000
|
-
supervisely-6.73.
|
|
1001
|
-
supervisely-6.73.
|
|
1002
|
-
supervisely-6.73.
|
|
1003
|
-
supervisely-6.73.
|
|
1004
|
-
supervisely-6.73.
|
|
1005
|
-
supervisely-6.73.
|
|
1000
|
+
supervisely-6.73.228.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
1001
|
+
supervisely-6.73.228.dist-info/METADATA,sha256=gEzztrS-yBBzf4a8JM9h_HWYtonbH_CrRzFgNqGr1Fs,33150
|
|
1002
|
+
supervisely-6.73.228.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
|
|
1003
|
+
supervisely-6.73.228.dist-info/entry_points.txt,sha256=U96-5Hxrp2ApRjnCoUiUhWMqijqh8zLR03sEhWtAcms,102
|
|
1004
|
+
supervisely-6.73.228.dist-info/top_level.txt,sha256=kcFVwb7SXtfqZifrZaSE3owHExX4gcNYe7Q2uoby084,28
|
|
1005
|
+
supervisely-6.73.228.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|