Skip to content

Commit 1a743e3

Browse files
authored
Merge pull request #29 from scaleapi/user/rkaplan/add-pytest
Add Pytest support and first unit tests
2 parents 1ae4335 + 69c1dff commit 1a743e3

File tree

5 files changed

+170
-16
lines changed

5 files changed

+170
-16
lines changed

conftest.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# grequests must be imported before any module not designed for reentrancy,
2+
# because it relies on aggressive monkey patching that breaks if done after many
3+
# other common module imports, e.g. ssl.
4+
#
5+
# So we import it before everything else. For details see:
6+
# https://github.com/gevent/gevent/issues/1016#issuecomment-328530533
7+
# https://github.com/spyoungtech/grequests/issues/8
8+
import grequests
9+
################
10+
11+
import logging
12+
import os
13+
14+
import requests
15+
import pytest
16+
17+
import nucleus
18+
from nucleus.constants import SUCCESS_STATUS_CODES
19+
20+
21+
assert 'NUCLEUS_PYTEST_API_KEY' in os.environ, \
22+
"You must set the 'NUCLEUS_PYTEST_API_KEY' environment variable to a valid " \
23+
"Nucleus API key to run the test suite"
24+
25+
API_KEY = os.environ['NUCLEUS_PYTEST_API_KEY']
26+
27+
28+
@pytest.fixture(scope='session')
29+
def monkeypatch_session(request):
30+
""" This workaround is needed to allow monkeypatching in session-scoped fixtures.
31+
32+
See https://github.com/pytest-dev/pytest/issues/363
33+
"""
34+
from _pytest.monkeypatch import MonkeyPatch
35+
mpatch = MonkeyPatch()
36+
yield mpatch
37+
mpatch.undo()
38+
39+
40+
@pytest.fixture(scope='session')
41+
def CLIENT(monkeypatch_session):
42+
client = nucleus.NucleusClient(API_KEY)
43+
44+
# Change _make_request to raise AsssertionErrors when the
45+
# HTTP status code is not successful, so that tests fail if
46+
# the request was unsuccessful.
47+
def _make_request_patch(
48+
payload: dict, route: str, requests_command=requests.post
49+
) -> dict:
50+
response = client._make_request_raw(payload, route, requests_command)
51+
assert response.status_code in SUCCESS_STATUS_CODES, \
52+
f"HTTP response had status code: {response.status_code}. " \
53+
f"Full JSON: {response.json()}"
54+
return response.json()
55+
56+
monkeypatch_session.setattr(client, "_make_request", _make_request_patch)
57+
return client

nucleus/__init__.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
ANNOTATIONS_PROCESSED_KEY,
9797
PREDICTIONS_PROCESSED_KEY,
9898
STATUS_CODE_KEY,
99+
SUCCESS_STATUS_CODES,
99100
DATASET_NAME_KEY,
100101
DATASET_MODEL_RUNS_KEY,
101102
DATASET_SLICES_KEY,
@@ -878,11 +879,13 @@ def _make_grequest(
878879
)
879880
return post
880881

881-
def _make_request(
882+
def _make_request_raw(
882883
self, payload: dict, route: str, requests_command=requests.post
883884
) -> dict:
884885
"""
885-
makes a request to Nucleus endpoint
886+
Makes a request to Nucleus endpoint. This method returns the raw
887+
requests.Response object which is useful for unit testing.
888+
886889
:param payload: given payload
887890
:param route: route for the request
888891
:param requests_command: requests.post, requests.get, requests.delete
@@ -899,7 +902,23 @@ def _make_request(
899902
)
900903
logger.info("API request has response code %s", response.status_code)
901904

902-
if response.status_code != 200:
905+
return response
906+
907+
def _make_request(
908+
self, payload: dict, route: str, requests_command=requests.post
909+
) -> dict:
910+
"""
911+
Makes a request to Nucleus endpoint and logs a warning if not
912+
successful.
913+
914+
:param payload: given payload
915+
:param route: route for the request
916+
:param requests_command: requests.post, requests.get, requests.delete
917+
:return: response JSON
918+
"""
919+
response = self._make_request_raw(payload, route, requests_command)
920+
921+
if response.status_code not in SUCCESS_STATUS_CODES:
903922
logger.warning(response)
904923

905924
return (

nucleus/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
ANNOTATIONS_PROCESSED_KEY = "annotations_processed"
1717
PREDICTIONS_PROCESSED_KEY = "predictions_processed"
1818
STATUS_CODE_KEY = "status_code"
19+
SUCCESS_STATUS_CODES = [200, 201]
1920
ERRORS_KEY = "errors"
2021
MODEL_RUN_ID_KEY = "model_run_id"
2122
MODEL_ID_KEY = "model_id"

requirements.txt

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,5 @@
1-
# Automatically generated by https://github.com/damnever/pigar.
2-
3-
# nucleus-python-client/nucleus/__init__.py: 63
4-
grequests == 0.6.0
5-
6-
# nucleus-python-client/nucleus/__init__.py: 64,65,68
7-
requests == 2.22.0
8-
9-
# nucleus-python-client/setup.py: 1
10-
setuptools == 46.0.0
11-
12-
# nucleus-python-client/nucleus/__init__.py: 60,61
13-
tqdm == 4.54.1
1+
grequests >= 0.6.0
2+
pytest >= 6.2.2
3+
requests >= 2.22.0
4+
setuptools >= 46.0.0
5+
tqdm >= 4.56.1

tests/test_dataset.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import pytest
2+
3+
from pathlib import Path
4+
5+
from nucleus import Dataset, DatasetItem, UploadResponse
6+
from nucleus.constants import (
7+
NEW_ITEMS,
8+
UPDATED_ITEMS,
9+
IGNORED_ITEMS,
10+
ERROR_ITEMS,
11+
ERROR_PAYLOAD,
12+
DATASET_ID_KEY,
13+
)
14+
15+
16+
TEST_DATASET_NAME = '[PyTest] Test Dataset'
17+
TEST_IMG_URLS = [
18+
's3://scaleapi-attachments/BDD/BDD/bdd100k/images/100k/train/6dd63871-831611a6.jpg',
19+
's3://scaleapi-attachments/BDD/BDD/bdd100k/images/100k/train/82c1005c-e2d1d94f.jpg',
20+
's3://scaleapi-attachments/BDD/BDD/bdd100k/images/100k/train/7f2e1814-6591087d.jpg',
21+
's3://scaleapi-attachments/BDD/BDD/bdd100k/images/100k/train/06924f46-1708b96f.jpg',
22+
's3://scaleapi-attachments/BDD/BDD/bdd100k/images/100k/train/89b42832-10d662f4.jpg',
23+
]
24+
25+
@pytest.fixture(scope='module')
26+
def dataset(CLIENT):
27+
ds = CLIENT.create_dataset(TEST_DATASET_NAME)
28+
yield ds
29+
30+
CLIENT.delete_dataset(ds.id)
31+
32+
33+
def test_dataset_create_and_delete(CLIENT):
34+
# Creation
35+
ds = CLIENT.create_dataset(TEST_DATASET_NAME)
36+
assert isinstance(ds, Dataset)
37+
assert ds.name == TEST_DATASET_NAME
38+
assert ds.model_runs == []
39+
assert ds.slices == []
40+
assert ds.size == 0
41+
assert ds.items == []
42+
43+
# Deletion
44+
response = CLIENT.delete_dataset(ds.id)
45+
assert response == {}
46+
47+
48+
def test_dataset_append(dataset):
49+
def check_is_expected_response(response):
50+
assert isinstance(response, UploadResponse)
51+
resp_json = response.json()
52+
assert resp_json[DATASET_ID_KEY] == dataset.id
53+
assert resp_json[NEW_ITEMS] == len(TEST_IMG_URLS)
54+
assert resp_json[UPDATED_ITEMS] == 0
55+
assert resp_json[IGNORED_ITEMS] == 0
56+
assert resp_json[ERROR_ITEMS] == 0
57+
assert ERROR_PAYLOAD not in resp_json
58+
59+
# Plain image upload
60+
ds_items_plain = []
61+
for url in TEST_IMG_URLS:
62+
ds_items_plain.append(DatasetItem(image_location=url))
63+
response = dataset.append(ds_items_plain)
64+
check_is_expected_response(response)
65+
66+
# With reference ids and metadata:
67+
ds_items_with_metadata = []
68+
for i, url in enumerate(TEST_IMG_URLS):
69+
ds_items_with_metadata.append(DatasetItem(
70+
image_location=url,
71+
reference_id=Path(url).name,
72+
metadata={
73+
'made_with_pytest': True,
74+
'example_int': i,
75+
'example_str': 'hello',
76+
'example_float': 0.5,
77+
'example_dict': {
78+
'nested': True,
79+
},
80+
'example_list': ['hello', i, False],
81+
}
82+
))
83+
84+
response = dataset.append(ds_items_with_metadata)
85+
check_is_expected_response(response)

0 commit comments

Comments
 (0)