unitlab 2.4.0__tar.gz → 2.4.2__tar.gz
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.
- {unitlab-2.4.0 → unitlab-2.4.2}/.claude/settings.local.json +10 -1
- {unitlab-2.4.0 → unitlab-2.4.2}/LICENSE.md +1 -1
- {unitlab-2.4.0 → unitlab-2.4.2}/PKG-INFO +5 -8
- {unitlab-2.4.0 → unitlab-2.4.2}/README.md +2 -2
- {unitlab-2.4.0 → unitlab-2.4.2}/pyproject.toml +3 -6
- {unitlab-2.4.0 → unitlab-2.4.2}/src/unitlab/cli.py +6 -13
- {unitlab-2.4.0 → unitlab-2.4.2}/src/unitlab/client.py +88 -73
- unitlab-2.4.0/QUICKSTART.MD +0 -24
- unitlab-2.4.0/assets/logo.png +0 -0
- unitlab-2.4.0/assets/unitlabDoc.png +0 -0
- {unitlab-2.4.0 → unitlab-2.4.2}/.gitignore +0 -0
- {unitlab-2.4.0 → unitlab-2.4.2}/src/unitlab/__init__.py +0 -0
- {unitlab-2.4.0 → unitlab-2.4.2}/src/unitlab/__main__.py +0 -0
- {unitlab-2.4.0 → unitlab-2.4.2}/src/unitlab/_config.py +0 -0
- {unitlab-2.4.0 → unitlab-2.4.2}/src/unitlab/exceptions.py +0 -0
|
@@ -32,7 +32,16 @@
|
|
|
32
32
|
"Bash(git reset:*)",
|
|
33
33
|
"Bash(git rm:*)",
|
|
34
34
|
"Bash(git status:*)",
|
|
35
|
-
"Bash(git push:*)"
|
|
35
|
+
"Bash(git push:*)",
|
|
36
|
+
"Bash(git update-ref:*)",
|
|
37
|
+
"Bash(gh api:*)",
|
|
38
|
+
"Bash(for user in Shahrullo shohmax mrakhror maxunitlab)",
|
|
39
|
+
"Bash(do echo \"Removing $user...\")",
|
|
40
|
+
"WebFetch(domain:unitlab-storage.s3.us-east-2.amazonaws.com)",
|
|
41
|
+
"Bash(ps:*)",
|
|
42
|
+
"WebFetch(domain:127.0.0.1)",
|
|
43
|
+
"Bash(DATASET_ID=\"b727a6fd-7786-4d40-b75b-56e7f52f378b\")",
|
|
44
|
+
"Bash(pipenv run:*)"
|
|
36
45
|
]
|
|
37
46
|
}
|
|
38
47
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: unitlab
|
|
3
|
-
Version: 2.4.
|
|
3
|
+
Version: 2.4.2
|
|
4
4
|
Summary: Python SDK for the Unitlab.ai data annotation platform
|
|
5
5
|
Project-URL: Homepage, https://unitlab.ai
|
|
6
6
|
Project-URL: Documentation, https://docs.unitlab.ai
|
|
@@ -19,25 +19,22 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.14
|
|
20
20
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
21
|
Requires-Python: >=3.10
|
|
22
|
-
Requires-Dist:
|
|
23
|
-
Requires-Dist: aiohttp>=3.9
|
|
24
|
-
Requires-Dist: requests>=2.28
|
|
22
|
+
Requires-Dist: httpx>=0.27
|
|
25
23
|
Requires-Dist: tqdm>=4.60
|
|
26
24
|
Requires-Dist: typer>=0.9
|
|
27
25
|
Requires-Dist: validators>=0.20
|
|
28
26
|
Provides-Extra: dev
|
|
29
|
-
Requires-Dist: aioresponses>=0.7; extra == 'dev'
|
|
30
27
|
Requires-Dist: build>=1.0; extra == 'dev'
|
|
31
28
|
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
32
29
|
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
33
|
-
Requires-Dist:
|
|
30
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
34
31
|
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
35
32
|
Requires-Dist: twine>=5.0; extra == 'dev'
|
|
36
33
|
Description-Content-Type: text/markdown
|
|
37
34
|
|
|
38
35
|
<p align="center">
|
|
39
36
|
<br>
|
|
40
|
-
<img src="
|
|
37
|
+
<img src="https://unitlab-storage.s3.us-east-2.amazonaws.com/Logo.png" width="400"/>
|
|
41
38
|
<br>
|
|
42
39
|
<p>
|
|
43
40
|
|
|
@@ -58,7 +55,7 @@ Description-Content-Type: text/markdown
|
|
|
58
55
|
|
|
59
56
|
[Unitlab.ai](https://unitlab.ai/) is an AI-driven data annotation platform that automates the collection of raw data, facilitating collaboration with human annotators to produce highly accurate labels for your machine learning models. With our service, you can optimize work efficiency, improve data quality, and reduce costs.
|
|
60
57
|
|
|
61
|
-

|
|
62
59
|
|
|
63
60
|
# Unitlab Python SDK
|
|
64
61
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<p align="center">
|
|
2
2
|
<br>
|
|
3
|
-
<img src="
|
|
3
|
+
<img src="https://unitlab-storage.s3.us-east-2.amazonaws.com/Logo.png" width="400"/>
|
|
4
4
|
<br>
|
|
5
5
|
<p>
|
|
6
6
|
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
[Unitlab.ai](https://unitlab.ai/) is an AI-driven data annotation platform that automates the collection of raw data, facilitating collaboration with human annotators to produce highly accurate labels for your machine learning models. With our service, you can optimize work efficiency, improve data quality, and reduce costs.
|
|
23
23
|
|
|
24
|
-

|
|
25
25
|
|
|
26
26
|
# Unitlab Python SDK
|
|
27
27
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "unitlab"
|
|
7
|
-
version = "2.4.
|
|
7
|
+
version = "2.4.2"
|
|
8
8
|
description = "Python SDK for the Unitlab.ai data annotation platform"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -23,9 +23,7 @@ classifiers = [
|
|
|
23
23
|
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
24
24
|
]
|
|
25
25
|
dependencies = [
|
|
26
|
-
"
|
|
27
|
-
"aiofiles>=23.0",
|
|
28
|
-
"requests>=2.28",
|
|
26
|
+
"httpx>=0.27",
|
|
29
27
|
"tqdm>=4.60",
|
|
30
28
|
"typer>=0.9",
|
|
31
29
|
"validators>=0.20",
|
|
@@ -35,8 +33,7 @@ dependencies = [
|
|
|
35
33
|
dev = [
|
|
36
34
|
"pytest>=7.0",
|
|
37
35
|
"pytest-asyncio>=0.21",
|
|
38
|
-
"
|
|
39
|
-
"aioresponses>=0.7",
|
|
36
|
+
"respx>=0.21",
|
|
40
37
|
"ruff>=0.4",
|
|
41
38
|
"build>=1.0",
|
|
42
39
|
"twine>=5.0",
|
|
@@ -39,7 +39,7 @@ class DownloadType(str, Enum):
|
|
|
39
39
|
|
|
40
40
|
class DatasetSplit(str, Enum):
|
|
41
41
|
train = "train"
|
|
42
|
-
|
|
42
|
+
validation = "validation"
|
|
43
43
|
test = "test"
|
|
44
44
|
|
|
45
45
|
|
|
@@ -120,24 +120,17 @@ def dataset_download(
|
|
|
120
120
|
typer.Option(help="Download type (annotation, files)"),
|
|
121
121
|
] = DownloadType.annotation,
|
|
122
122
|
export_type: Annotated[
|
|
123
|
-
str,
|
|
123
|
+
str,
|
|
124
|
+
typer.Option(help="Export type (e.g. COCO, YOLOv8, YOLOv5, UUEF)"),
|
|
124
125
|
] = "COCO",
|
|
125
126
|
split_type: Annotated[
|
|
126
|
-
DatasetSplit, typer.Option(help="Dataset split (train,
|
|
127
|
+
DatasetSplit, typer.Option(help="Dataset split (train, validation, test)")
|
|
127
128
|
] = DatasetSplit.train,
|
|
128
129
|
):
|
|
129
130
|
if download_type == DownloadType.annotation:
|
|
130
|
-
|
|
131
|
-
raise typer.BadParameter(
|
|
132
|
-
"Export type is required when download type is annotation"
|
|
133
|
-
)
|
|
134
|
-
if not split_type:
|
|
135
|
-
raise typer.BadParameter(
|
|
136
|
-
"Split type is required when download type is annotation"
|
|
137
|
-
)
|
|
138
|
-
get_client(api_key).dataset_download(pk, export_type, split_type)
|
|
131
|
+
get_client(api_key).dataset_download(str(pk), export_type, split_type.value)
|
|
139
132
|
else:
|
|
140
|
-
typer.echo(get_client(api_key).dataset_download_files(pk))
|
|
133
|
+
typer.echo(get_client(api_key).dataset_download_files(str(pk)))
|
|
141
134
|
|
|
142
135
|
|
|
143
136
|
if __name__ == "__main__":
|
|
@@ -6,11 +6,9 @@ import logging
|
|
|
6
6
|
import os
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import Any
|
|
9
|
-
from urllib.parse import
|
|
9
|
+
from urllib.parse import urlparse
|
|
10
10
|
|
|
11
|
-
import
|
|
12
|
-
import aiohttp
|
|
13
|
-
import requests
|
|
11
|
+
import httpx
|
|
14
12
|
import tqdm
|
|
15
13
|
|
|
16
14
|
from ._config import get_api_key, get_api_url
|
|
@@ -28,6 +26,27 @@ _UPLOAD_CONCURRENCY = 20
|
|
|
28
26
|
_DOWNLOAD_CONCURRENCY = 50
|
|
29
27
|
|
|
30
28
|
|
|
29
|
+
def _extract_error_message(response: httpx.Response) -> str:
|
|
30
|
+
"""Extract a human-readable message from an error response."""
|
|
31
|
+
try:
|
|
32
|
+
body = response.json()
|
|
33
|
+
if isinstance(body, dict):
|
|
34
|
+
for key in ("detail", "message"):
|
|
35
|
+
if key in body:
|
|
36
|
+
return str(body[key])
|
|
37
|
+
parts = []
|
|
38
|
+
for field, errors in body.items():
|
|
39
|
+
if isinstance(errors, list):
|
|
40
|
+
parts.append(f"{field}: {', '.join(str(e) for e in errors)}")
|
|
41
|
+
else:
|
|
42
|
+
parts.append(f"{field}: {errors}")
|
|
43
|
+
if parts:
|
|
44
|
+
return "; ".join(parts)
|
|
45
|
+
except (ValueError, KeyError):
|
|
46
|
+
pass
|
|
47
|
+
return response.text
|
|
48
|
+
|
|
49
|
+
|
|
31
50
|
def handle_exceptions(f):
|
|
32
51
|
"""Catch exceptions and throw Unitlab exceptions."""
|
|
33
52
|
|
|
@@ -37,23 +56,23 @@ def handle_exceptions(f):
|
|
|
37
56
|
r = f(self, *args, **kwargs)
|
|
38
57
|
r.raise_for_status()
|
|
39
58
|
return r.json()
|
|
40
|
-
except
|
|
59
|
+
except httpx.TimeoutException as e:
|
|
41
60
|
raise TimeoutError(message=str(e), detail=e) from e
|
|
42
|
-
except
|
|
43
|
-
text = e.response
|
|
61
|
+
except httpx.HTTPStatusError as e:
|
|
62
|
+
text = _extract_error_message(e.response)
|
|
44
63
|
if e.response.status_code == 401:
|
|
45
64
|
raise AuthenticationError(
|
|
46
65
|
message="Authentication failed", detail=e
|
|
47
66
|
) from e
|
|
48
67
|
if e.response.status_code == 403:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
) from e
|
|
53
|
-
if "not found" in text:
|
|
68
|
+
msg = f"Forbidden: {e.response.status_code} {e.response.reason_phrase}"
|
|
69
|
+
raise SubscriptionError(message=msg, detail=e) from e
|
|
70
|
+
if e.response.status_code == 404 or "not found" in text.lower():
|
|
54
71
|
raise NotFoundError(message=text, detail=e) from e
|
|
55
72
|
raise NetworkError(message=text, detail=e) from e
|
|
56
|
-
except
|
|
73
|
+
except ValueError as e:
|
|
74
|
+
raise NetworkError(message="Unexpected response format", detail=e) from e
|
|
75
|
+
except httpx.HTTPError as e:
|
|
57
76
|
raise NetworkError(message=str(e), detail=e) from e
|
|
58
77
|
|
|
59
78
|
return wrapper
|
|
@@ -123,11 +142,12 @@ class UnitlabClient:
|
|
|
123
142
|
self.api_url: str = (
|
|
124
143
|
api_url or os.environ.get("UNITLAB_API_URL") or get_api_url()
|
|
125
144
|
)
|
|
126
|
-
self.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
145
|
+
self._client = httpx.Client(
|
|
146
|
+
base_url=self.api_url,
|
|
147
|
+
headers={"Authorization": f"Api-Key {self.api_key}"},
|
|
148
|
+
transport=httpx.HTTPTransport(retries=3),
|
|
149
|
+
timeout=60.0,
|
|
150
|
+
)
|
|
131
151
|
|
|
132
152
|
def close(self) -> None:
|
|
133
153
|
"""Close :class:`UnitlabClient` connections.
|
|
@@ -147,7 +167,7 @@ class UnitlabClient:
|
|
|
147
167
|
with UnitlabClient() as client:
|
|
148
168
|
client.projects()
|
|
149
169
|
"""
|
|
150
|
-
self.
|
|
170
|
+
self._client.close()
|
|
151
171
|
|
|
152
172
|
def __enter__(self) -> UnitlabClient:
|
|
153
173
|
return self
|
|
@@ -160,21 +180,15 @@ class UnitlabClient:
|
|
|
160
180
|
) -> None:
|
|
161
181
|
self.close()
|
|
162
182
|
|
|
163
|
-
def _get_headers(self) -> dict[str, str]:
|
|
164
|
-
return {"Authorization": f"Api-Key {self.api_key}"}
|
|
165
|
-
|
|
166
183
|
@handle_exceptions
|
|
167
|
-
def _get(self, endpoint: str) ->
|
|
168
|
-
return self.
|
|
184
|
+
def _get(self, endpoint: str) -> httpx.Response:
|
|
185
|
+
return self._client.get(endpoint)
|
|
169
186
|
|
|
170
187
|
@handle_exceptions
|
|
171
188
|
def _post(
|
|
172
189
|
self, endpoint: str, data: dict[str, Any] | None = None
|
|
173
|
-
) ->
|
|
174
|
-
return self.
|
|
175
|
-
urljoin(self.api_url, endpoint),
|
|
176
|
-
json=data if data is not None else {},
|
|
177
|
-
)
|
|
190
|
+
) -> httpx.Response:
|
|
191
|
+
return self._client.post(endpoint, json=data if data is not None else {})
|
|
178
192
|
|
|
179
193
|
def projects(self, pretty: int = 0) -> Any:
|
|
180
194
|
return self._get(f"/api/sdk/projects/?pretty={pretty}")
|
|
@@ -234,25 +248,22 @@ class UnitlabClient:
|
|
|
234
248
|
semaphore = asyncio.Semaphore(_UPLOAD_CONCURRENCY)
|
|
235
249
|
|
|
236
250
|
async def post_file(
|
|
237
|
-
|
|
251
|
+
client: httpx.AsyncClient, file: str, project_id: str
|
|
238
252
|
) -> int:
|
|
239
|
-
async with semaphore
|
|
240
|
-
|
|
241
|
-
form_data.add_field(
|
|
242
|
-
"file", await f.read(), filename=os.path.basename(file)
|
|
243
|
-
)
|
|
253
|
+
async with semaphore:
|
|
254
|
+
extra_data: dict[str, str] = {}
|
|
244
255
|
if generic_type == "text":
|
|
245
|
-
|
|
256
|
+
extra_data["sentences_per_chunk"] = str(sentences_per_chunk)
|
|
246
257
|
elif generic_type == "video":
|
|
247
|
-
|
|
258
|
+
extra_data["fps"] = str(fps)
|
|
248
259
|
|
|
249
260
|
try:
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
261
|
+
with open(file, "rb") as f:
|
|
262
|
+
response = await client.post(
|
|
263
|
+
f"/api/sdk/projects/{project_id}/upload-data/",
|
|
264
|
+
files={"file": (os.path.basename(file), f)},
|
|
265
|
+
data=extra_data,
|
|
266
|
+
)
|
|
256
267
|
response.raise_for_status()
|
|
257
268
|
return 1
|
|
258
269
|
except Exception as e:
|
|
@@ -262,19 +273,18 @@ class UnitlabClient:
|
|
|
262
273
|
async def main() -> None:
|
|
263
274
|
logger.info(f"Uploading {num_files} files to project {project_id}")
|
|
264
275
|
with tqdm.tqdm(total=num_files, ncols=80) as pbar:
|
|
265
|
-
async with
|
|
266
|
-
|
|
267
|
-
|
|
276
|
+
async with httpx.AsyncClient(
|
|
277
|
+
base_url=self.api_url,
|
|
278
|
+
headers={"Authorization": f"Api-Key {self.api_key}"},
|
|
279
|
+
timeout=600.0,
|
|
280
|
+
) as client:
|
|
268
281
|
for i in range(num_batches):
|
|
269
|
-
tasks = [
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
session=session, file=file, project_id=project_id
|
|
276
|
-
)
|
|
277
|
-
)
|
|
282
|
+
tasks = [
|
|
283
|
+
post_file(client=client, file=file, project_id=project_id)
|
|
284
|
+
for file in filtered_files[
|
|
285
|
+
i * batch_size : min((i + 1) * batch_size, num_files)
|
|
286
|
+
]
|
|
287
|
+
]
|
|
278
288
|
for f in asyncio.as_completed(tasks):
|
|
279
289
|
pbar.update(await f)
|
|
280
290
|
|
|
@@ -295,17 +305,21 @@ class UnitlabClient:
|
|
|
295
305
|
},
|
|
296
306
|
)
|
|
297
307
|
|
|
298
|
-
|
|
299
|
-
|
|
308
|
+
file_url = response["file"]
|
|
309
|
+
try:
|
|
310
|
+
with httpx.stream("GET", file_url, timeout=300.0) as r:
|
|
311
|
+
r.raise_for_status()
|
|
300
312
|
|
|
301
|
-
|
|
302
|
-
|
|
313
|
+
parsed_url = urlparse(file_url)
|
|
314
|
+
filename = os.path.basename(parsed_url.path)
|
|
303
315
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
316
|
+
with open(filename, "wb") as f:
|
|
317
|
+
for chunk in r.iter_bytes():
|
|
318
|
+
f.write(chunk)
|
|
319
|
+
logger.info(f"File: {os.path.abspath(filename)}")
|
|
320
|
+
return os.path.abspath(filename)
|
|
321
|
+
except httpx.HTTPError as e:
|
|
322
|
+
raise NetworkError(message=f"Failed to download file: {e}", detail=e) from e
|
|
309
323
|
|
|
310
324
|
def dataset_download_files(self, dataset_id: str) -> str:
|
|
311
325
|
"""Download files from a dataset.
|
|
@@ -350,26 +364,27 @@ class UnitlabClient:
|
|
|
350
364
|
semaphore = asyncio.Semaphore(_DOWNLOAD_CONCURRENCY)
|
|
351
365
|
|
|
352
366
|
async def download_file(
|
|
353
|
-
|
|
367
|
+
client: httpx.AsyncClient, dataset_file: dict[str, str]
|
|
354
368
|
) -> int:
|
|
355
|
-
async with semaphore
|
|
369
|
+
async with semaphore:
|
|
356
370
|
try:
|
|
357
|
-
|
|
371
|
+
async with client.stream("GET", dataset_file["source"]) as r:
|
|
372
|
+
r.raise_for_status()
|
|
373
|
+
with open(dataset_file["file_path"], "wb") as f:
|
|
374
|
+
async for chunk in r.aiter_bytes():
|
|
375
|
+
f.write(chunk)
|
|
376
|
+
return 1
|
|
358
377
|
except Exception as e:
|
|
359
378
|
logger.error(
|
|
360
379
|
f"Error downloading file {dataset_file['file_name']} - {e}"
|
|
361
380
|
)
|
|
362
381
|
return 0
|
|
363
|
-
async with aiofiles.open(dataset_file["file_path"], "wb") as f:
|
|
364
|
-
async for chunk in r.content.iter_any():
|
|
365
|
-
await f.write(chunk)
|
|
366
|
-
return 1
|
|
367
382
|
|
|
368
383
|
async def main() -> None:
|
|
369
384
|
with tqdm.tqdm(total=len(files_to_download), ncols=80) as pbar:
|
|
370
|
-
async with
|
|
385
|
+
async with httpx.AsyncClient(timeout=600.0) as client:
|
|
371
386
|
tasks = [
|
|
372
|
-
download_file(
|
|
387
|
+
download_file(client=client, dataset_file=df)
|
|
373
388
|
for df in files_to_download
|
|
374
389
|
]
|
|
375
390
|
for f in asyncio.as_completed(tasks):
|
unitlab-2.4.0/QUICKSTART.MD
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
First install the Unitlab.ai Python SDK using pip:
|
|
2
|
-
pip install --upgrade unitlab
|
|
3
|
-
|
|
4
|
-
Import the necessary packages, and initialize the Unitlab client using your API key:
|
|
5
|
-
from unitlab import UnitlabClient
|
|
6
|
-
|
|
7
|
-
#An API key can be created https://app.unitlab.ai/UnitlabAI/api-keys.
|
|
8
|
-
api_key = "YOUR API KEY"
|
|
9
|
-
client = UnitlabClient(api_key)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
Let's get a list of your projects programmatically using client.projects().
|
|
13
|
-
Note that this list will be retrieved under the user account corresponding to the API key.
|
|
14
|
-
#Result:
|
|
15
|
-
[{'pk': '0ed69772-fd28-471b-b91c-2a57368c0dfb', 'name': '', 'parent_name': None, 'invoice': {'price': '0.00', 'images': 0}, 'annotator_progress': 0.0, 'reviewer_progress': 0.0, 'creator': 'user@gmail.com', 'created': '2023-04-11T11:00:23.737326+05:00'}, {'pk': '34738aec-b69b-4d30-9763-653017a68c02', 'name': 'human', 'parent_name': 'Red Blood Cell', 'invoice': {'price': '100.00', 'images': 1}, 'annotator_progress': 0.0, 'reviewer_progress': 0.0, 'creator': 'user@gmail.com', 'created': '2023-04-11T16:57:01.767677+05:00'},]
|
|
16
|
-
|
|
17
|
-
Add images to the project.
|
|
18
|
-
Now let's add images to the project which has not been created yet completely using client.project_upload_data():
|
|
19
|
-
#Code
|
|
20
|
-
project_id = 'project_id' # a project id.
|
|
21
|
-
folder = '/home/user/DirOfPics/' # you can upload pictures(.jpg, .png) locally.
|
|
22
|
-
sample = client.project_upload_data(project_id, folder)
|
|
23
|
-
|
|
24
|
-
We can verify them using client.project('project_id') when project is fully created and images will be added to your images to do the project.
|
unitlab-2.4.0/assets/logo.png
DELETED
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|