Skip to content

Commit c026180

Browse files
author
Diego Ardila
committed
Add docs
1 parent 50b1116 commit c026180

File tree

3 files changed

+107
-48
lines changed

3 files changed

+107
-48
lines changed

nucleus/__init__.py

Lines changed: 87 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -50,25 +50,23 @@
5050
geometry | dict | Representation of the bounding box in the Box2DGeometry format.\n
5151
metadata | dict | An arbitrary metadata blob for the annotation.\n
5252
"""
53+
import asyncio
5354
import json
5455
import logging
5556
import os
5657
from typing import Any, Dict, List, Optional, Union
5758

59+
import aiohttp
5860
import pkg_resources
5961
import requests
6062
import tqdm
6163
import tqdm.notebook as tqdm_notebook
6264

63-
# pylint: disable=E1101
64-
# TODO: refactor to reduce this file to under 1000 lines.
65-
# pylint: disable=C0302
66-
6765
from .annotation import (
6866
BoxAnnotation,
6967
PolygonAnnotation,
70-
SegmentationAnnotation,
7168
Segment,
69+
SegmentationAnnotation,
7270
)
7371
from .constants import (
7472
ANNOTATION_METADATA_SCHEMA_KEY,
@@ -122,6 +120,11 @@
122120
from .slice import Slice
123121
from .upload_response import UploadResponse
124122

123+
# pylint: disable=E1101
124+
# TODO: refactor to reduce this file to under 1000 lines.
125+
# pylint: disable=C0302
126+
127+
125128
__version__ = pkg_resources.get_distribution("scale-nucleus").version
126129

127130
logger = logging.getLogger(__name__)
@@ -141,7 +144,6 @@ def __init__(
141144
api_key: str,
142145
use_notebook: bool = False,
143146
endpoint: str = None,
144-
verify: bool = True,
145147
):
146148
self.api_key = api_key
147149
self.tqdm_bar = tqdm.tqdm
@@ -424,13 +426,12 @@ def get_files(batch):
424426
files_per_request.append(get_files(batch))
425427
payload_items.append(batch)
426428

427-
responses = [
428-
self._make_files_request(
429-
files=files,
430-
route=f"dataset/{dataset_id}/append",
429+
responses = asyncio.run(
430+
self.make_many_files_requests_asynchronously(
431+
files_per_request,
432+
f"dataset/{dataset_id}/append",
431433
)
432-
for files in files_per_request
433-
]
434+
)
434435

435436
def close_files(request_items):
436437
for item in request_items:
@@ -444,6 +445,70 @@ def close_files(request_items):
444445

445446
return responses
446447

448+
async def make_many_files_requests_asynchronously(
449+
self, files_per_request, route
450+
):
451+
"""
452+
Makes an async post request with files to a Nucleus endpoint.
453+
454+
:param files_per_request: A list of lists of tuples (name, (filename, file_pointer, content_type))
455+
name will become the name by which the multer can build an array.
456+
:param route: route for the request
457+
:return: awaitable list(response)
458+
"""
459+
async with aiohttp.ClientSession() as session:
460+
tasks = [
461+
asyncio.ensure_future(
462+
self._make_files_request(
463+
files=files, route=route, session=session
464+
)
465+
)
466+
for files in files_per_request
467+
]
468+
return await asyncio.gather(*tasks)
469+
470+
async def _make_files_request(
471+
self,
472+
files,
473+
route: str,
474+
session: aiohttp.ClientSession,
475+
):
476+
"""
477+
Makes an async post request with files to a Nucleus endpoint.
478+
479+
:param files: A list of tuples (filename, file_pointer, file_type)
480+
:param route: route for the request
481+
:param session: Session to use for post.
482+
:return: response
483+
"""
484+
endpoint = f"{self.endpoint}/{route}"
485+
486+
logger.info("Posting to %s", endpoint)
487+
488+
form = aiohttp.FormData()
489+
490+
for file in files:
491+
form.add_field(
492+
name=file[0],
493+
filename=file[1][0],
494+
value=file[1][1],
495+
content_type=file[1][2],
496+
)
497+
498+
async with session.post(
499+
endpoint,
500+
data=form,
501+
auth=aiohttp.BasicAuth(self.api_key, ""),
502+
timeout=DEFAULT_NETWORK_TIMEOUT_SEC,
503+
) as response:
504+
logger.info("API request has response code %s", response.status)
505+
if not response.ok:
506+
self.handle_bad_response(
507+
endpoint, session.post, aiohttp_response=response
508+
)
509+
510+
return await response.json()
511+
447512
def _process_append_requests(
448513
self,
449514
dataset_id: str,
@@ -1013,35 +1078,6 @@ def delete_custom_index(self, dataset_id: str):
10131078
requests_command=requests.delete,
10141079
)
10151080

1016-
def _make_files_request(
1017-
self, files, route: str, requests_command=requests.post
1018-
):
1019-
"""
1020-
Makes a request to Nucleus endpoint. This method returns the raw
1021-
requests.Response object which is useful for unit testing.
1022-
1023-
:param payload: given payload
1024-
:param endpoint: endpoint + route for the request
1025-
:param requests_command: requests.post, requests.get, requests.delete
1026-
:return: response
1027-
"""
1028-
endpoint = f"{self.endpoint}/{route}"
1029-
1030-
logger.info("Posting to %s", endpoint)
1031-
1032-
response = requests_command(
1033-
endpoint,
1034-
files=files,
1035-
auth=(self.api_key, ""),
1036-
timeout=DEFAULT_NETWORK_TIMEOUT_SEC,
1037-
)
1038-
logger.info("API request has response code %s", response.status_code)
1039-
1040-
if not response.ok:
1041-
self.handle_bad_response(endpoint, requests_command, response)
1042-
1043-
return response.json()
1044-
10451081
def make_request(
10461082
self, payload: dict, route: str, requests_command=requests.post
10471083
) -> dict:
@@ -1072,5 +1108,13 @@ def make_request(
10721108

10731109
return response.json()
10741110

1075-
def handle_bad_response(self, endpoint, requests_command, response):
1076-
raise NucleusAPIError(endpoint, requests_command, response)
1111+
def handle_bad_response(
1112+
self,
1113+
endpoint,
1114+
requests_command,
1115+
requests_response=None,
1116+
aiohttp_response=None,
1117+
):
1118+
raise NucleusAPIError(
1119+
endpoint, requests_command, requests_response, aiohttp_response
1120+
)

nucleus/errors.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,23 @@ def __init__(self, message="Could not retrieve dataset items"):
2525

2626

2727
class NucleusAPIError(Exception):
28-
def __init__(self, endpoint, command, response):
29-
message = f"Tried to {command.__name__} {endpoint}, but received {response.status_code}: {response.reason}."
30-
if hasattr(response, "text"):
31-
if response.text:
32-
message += f"\nThe detailed error is:\n{response.text}"
28+
def __init__(
29+
self, endpoint, command, requests_response=None, aiohttp_response=None
30+
):
31+
32+
if requests_response is not None:
33+
message = f"Tried to {command.__name__} {endpoint}, but received {requests_response.status_code}: {requests_response.reason}."
34+
if hasattr(requests_response, "text"):
35+
if requests_response.text:
36+
message += (
37+
f"\nThe detailed error is:\n{requests_response.text}"
38+
)
39+
40+
if aiohttp_response is not None:
41+
message = f"Tried to {command.__name__} {endpoint}, but received {aiohttp_response.status}: {aiohttp_response.reason}."
42+
if hasattr(requests_response, "text"):
43+
text = requests_response.text()
44+
if text:
45+
message += f"\nThe detailed error is:\n{text}"
46+
3347
super().__init__(message)

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ python = "^3.6.2"
3636
requests = "^2.25.1"
3737
tqdm = "^4.41.0"
3838
dataclasses = { version = "^0.7", python = "^3.6.1, <3.7" }
39+
aiohttp = "^3.7.4"
3940

4041
[tool.poetry.dev-dependencies]
4142
poetry = "^1.1.5"

0 commit comments

Comments
 (0)