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.
@@ -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
  MIT License
2
2
 
3
- Copyright (c) 2024 Team Unitlab
3
+ Copyright (c) 2026 Team Unitlab
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unitlab
3
- Version: 2.4.0
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: aiofiles>=23.0
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: responses>=0.23; extra == 'dev'
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="assets/logo.png" width="400"/>
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
- ![](assets/unitlabDoc.png)
58
+ ![](https://unitlab-storage.s3.us-east-2.amazonaws.com/unitlab.png)
62
59
 
63
60
  # Unitlab Python SDK
64
61
 
@@ -1,6 +1,6 @@
1
1
  <p align="center">
2
2
  <br>
3
- <img src="assets/logo.png" width="400"/>
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
- ![](assets/unitlabDoc.png)
24
+ ![](https://unitlab-storage.s3.us-east-2.amazonaws.com/unitlab.png)
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.0"
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
- "aiohttp>=3.9",
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
- "responses>=0.23",
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
- val = "val"
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, typer.Option(help="Export type (COCO, YOLOv8, YOLOv5 ..etc)")
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, val, test)")
127
+ DatasetSplit, typer.Option(help="Dataset split (train, validation, test)")
127
128
  ] = DatasetSplit.train,
128
129
  ):
129
130
  if download_type == DownloadType.annotation:
130
- if not export_type:
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 urljoin, urlparse
9
+ from urllib.parse import urlparse
10
10
 
11
- import aiofiles
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 requests.exceptions.Timeout as e:
59
+ except httpx.TimeoutException as e:
41
60
  raise TimeoutError(message=str(e), detail=e) from e
42
- except requests.exceptions.HTTPError as e:
43
- text = e.response.text.lower()
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
- raise SubscriptionError(
50
- message=f"Forbidden: {e.response.status_code} {e.response.reason}",
51
- detail=e,
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 requests.exceptions.RequestException as e:
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.api_session = requests.Session()
127
- self.api_session.headers["Authorization"] = f"Api-Key {self.api_key}"
128
- adapter = requests.adapters.HTTPAdapter(max_retries=3)
129
- self.api_session.mount("http://", adapter)
130
- self.api_session.mount("https://", adapter)
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.api_session.close()
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) -> requests.Response:
168
- return self.api_session.get(urljoin(self.api_url, endpoint))
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
- ) -> requests.Response:
174
- return self.api_session.post(
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
- session: aiohttp.ClientSession, file: str, project_id: str
251
+ client: httpx.AsyncClient, file: str, project_id: str
238
252
  ) -> int:
239
- async with semaphore, aiofiles.open(file, "rb") as f:
240
- form_data = aiohttp.FormData()
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
- form_data.add_field("sentences_per_chunk", str(sentences_per_chunk))
256
+ extra_data["sentences_per_chunk"] = str(sentences_per_chunk)
246
257
  elif generic_type == "video":
247
- form_data.add_field("fps", str(fps))
258
+ extra_data["fps"] = str(fps)
248
259
 
249
260
  try:
250
- async with session.post(
251
- urljoin(
252
- self.api_url, f"/api/sdk/projects/{project_id}/upload-data/"
253
- ),
254
- data=form_data,
255
- ) as response:
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 aiohttp.ClientSession(
266
- headers=self._get_headers()
267
- ) as session:
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
- for file in filtered_files[
271
- i * batch_size : min((i + 1) * batch_size, num_files)
272
- ]:
273
- tasks.append(
274
- post_file(
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
- with requests.get(url=response["file"], stream=True) as r:
299
- r.raise_for_status()
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
- parsed_url = urlparse(response["file"])
302
- filename = os.path.basename(parsed_url.path)
313
+ parsed_url = urlparse(file_url)
314
+ filename = os.path.basename(parsed_url.path)
303
315
 
304
- with open(filename, "wb") as f:
305
- for chunk in r.iter_content(chunk_size=1024 * 1024):
306
- f.write(chunk)
307
- logger.info(f"File: {os.path.abspath(filename)}")
308
- return os.path.abspath(filename)
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
- session: aiohttp.ClientSession, dataset_file: dict[str, str]
367
+ client: httpx.AsyncClient, dataset_file: dict[str, str]
354
368
  ) -> int:
355
- async with semaphore, session.get(url=dataset_file["source"]) as r:
369
+ async with semaphore:
356
370
  try:
357
- r.raise_for_status()
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 aiohttp.ClientSession() as session:
385
+ async with httpx.AsyncClient(timeout=600.0) as client:
371
386
  tasks = [
372
- download_file(session=session, dataset_file=df)
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):
@@ -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.
Binary file
Binary file
File without changes
File without changes
File without changes
File without changes