Skip to content

Commit 3696f7f

Browse files
author
Matt Sokoloff
committed
restore tests
1 parent 9a58f9d commit 3696f7f

File tree

11 files changed

+110
-43
lines changed

11 files changed

+110
-43
lines changed

.github/workflows/python-package.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ jobs:
6767
env:
6868
# make sure to tell tox to use these environs in tox.ini
6969
#
70-
# randall@labelbox.com
70+
# msokoloff+prod-python@labelbox.com
7171
LABELBOX_TEST_API_KEY_PROD: ${{ secrets.LABELBOX_API_KEY }}
7272

7373
# randall+staging-python@labelbox.com

Dockerfile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
FROM python:3.6
1+
FROM python:3.7
2+
3+
RUN pip install pytest
24

35
COPY . /usr/src/labelbox
46
WORKDIR /usr/src/labelbox
57

6-
RUN pip install pytest
78
RUN python setup.py install

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ build:
55
test-staging: build
66
docker run -it -v ${PWD}:/usr/src -w /usr/src \
77
-e LABELBOX_TEST_ENVIRON="staging" \
8-
-e LABELBOX_TEST_API_KEY_STAGING="<REPLACE>" \
8+
-e LABELBOX_TEST_API_KEY_STAGING=${LABELBOX_TEST_API_KEY_STAGING} \
99
local/labelbox-python:test pytest $(PATH_TO_TEST) -svvx
1010

1111
test-prod: build
1212
docker run -it -v ${PWD}:/usr/src -w /usr/src \
1313
-e LABELBOX_TEST_ENVIRON="prod" \
14-
-e LABELBOX_TEST_API_KEY_PROD="<REPLACE>" \
14+
-e LABELBOX_TEST_API_KEY_PROD=${LABELBOX_TEST_API_KEY_PROD} \
1515
local/labelbox-python:test pytest $(PATH_TO_TEST) -svvx

labelbox/client.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from datetime import datetime, timezone
22
import json
3+
from labelbox.schema.ontology import Ontology
34
import logging
45
import mimetypes
56
import os
@@ -71,6 +72,7 @@ def __init__(self,
7172
'X-User-Agent': f'python-sdk {SDK_VERSION}'
7273
}
7374

75+
#TODO: Add exponential backoff so we don'tt overwhelm the api
7476
@retry.Retry(predicate=retry.if_exception_type(
7577
labelbox.exceptions.InternalServerError))
7678
def execute(self, query, params=None, timeout=10.0):
@@ -126,25 +128,23 @@ def convert_value(value):
126128
logger.debug("Response: %s", response.text)
127129
except requests.exceptions.Timeout as e:
128130
raise labelbox.exceptions.TimeoutError(str(e))
129-
130131
except requests.exceptions.RequestException as e:
131132
logger.error("Unknown error: %s", str(e))
132133
raise labelbox.exceptions.NetworkError(e)
133-
134134
except Exception as e:
135135
raise labelbox.exceptions.LabelboxError(
136136
"Unknown error during Client.query(): " + str(e), e)
137-
138137
try:
139138
r_json = response.json()
140139
except:
141-
error_502 = '502 Bad Gateway'
142-
if error_502 in response.text:
143-
raise labelbox.exceptions.InternalServerError(error_502)
144140
if "upstream connect error or disconnect/reset before headers" \
145141
in response.text:
146142
raise labelbox.exceptions.InternalServerError(
147143
"Connection reset")
144+
elif response.status_code == 502:
145+
error_502 = '502 Bad Gateway'
146+
raise labelbox.exceptions.InternalServerError(error_502)
147+
148148
raise labelbox.exceptions.LabelboxError(
149149
"Failed to parse response as JSON: %s" % response.text)
150150

@@ -189,6 +189,7 @@ def check_errors(keywords, *path):
189189

190190
# Check if API limit was exceeded
191191
response_msg = r_json.get("message", "")
192+
192193
if response_msg.startswith("You have exceeded"):
193194
raise labelbox.exceptions.ApiLimitError(response_msg)
194195

@@ -292,7 +293,6 @@ def upload_data(self,
292293
"1": (filename, content, content_type) if
293294
(filename and content_type) else content
294295
})
295-
296296
try:
297297
file_data = response.json().get("data", None)
298298
except ValueError as e: # response is not valid JSON
@@ -415,6 +415,12 @@ def get_datasets(self, where=None):
415415
"""
416416
return self._get_all(Dataset, where)
417417

418+
def get_ontologies(self, where=None):
419+
"""
420+
#TODO
421+
"""
422+
return self._get_all(Ontology, where)
423+
418424
def get_labeling_frontends(self, where=None):
419425
""" Fetches all the labeling frontends.
420426
@@ -473,7 +479,9 @@ def create_dataset(self, **kwargs):
473479
"""
474480
return self._create(Dataset, kwargs)
475481

476-
def create_project(self, **kwargs):
482+
def create_project(self,
483+
ontology=None,
484+
**kwargs): #<<<<< TODO: Do we want that signature
477485
""" Creates a Project object on the server.
478486
479487
Attribute values are passed as keyword arguments.

labelbox/schema/labeling_frontend.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ class LabelingFrontendOptions(DbObject):
3535
organization (Relationship): `ToOne` relationship to Organization
3636
"""
3737
customization_options = Field.String("customization_options")
38-
3938
project = Relationship.ToOne("Project")
4039
labeling_frontend = Relationship.ToOne("LabelingFrontend")
4140
organization = Relationship.ToOne("Organization")

labelbox/schema/ontology.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,51 @@ def from_json(cls, json_dict):
6363
return cls(**_dict)
6464

6565

66+
"""
67+
* The reason that an ontology is read only is because it is a second class citizen to labeling front end options.
68+
** This is because it is a more specific implementation of this.
69+
70+
- However, we want to support ontologies as if they were labeling front ends.
71+
- With this special relationship we can override the default behavior to mock the appropriate changes to the labeling front end
72+
73+
74+
###Note: The only problem is that you can't just create a stand alone ontology. right?
75+
# - Since you need to create a project and query the project ontology before one exists.
76+
77+
^^^^^^^ This is the worst. Even with hackery, you can't force a DB entry without create a new proj :(
78+
However, labeling front-ends cannot be created without projects either! So maybe we just copy the use cases of that.
79+
Use this as the simpler interface and make it clear that this is just a limited version
80+
81+
"""
82+
83+
84+
class OntologyRelationship(Relationship):
85+
86+
def __get__(self, parent):
87+
if not self.parent:
88+
self.parent = parent
89+
return self
90+
91+
def __init__(self):
92+
super(OntologyRelationship, self).__init__()
93+
self.parent = None
94+
95+
def __call__(self):
96+
if self.parent.setup_complete is None:
97+
#As it currently stands, it creates a new ontology with no new tools and the ontology cannot be edited.
98+
return None
99+
return super().__call__
100+
101+
def connect(self, other_ontology):
102+
if not isinstance(other_ontology, OntologyRelationship):
103+
raise Exception("only support ")
104+
105+
def disconnect(self):
106+
raise Exception(
107+
"Disconnect is not supported for Onotlogy. Instead connect another ontology to replace the current one."
108+
)
109+
110+
66111
class Ontology(DbObject):
67112
"""An ontology specifies which tools and classifications are available
68113
to a project. This is read only for now.

labelbox/schema/project.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,16 @@ def review_metrics(self, net_score):
221221
res = self.client.execute(query_str, {id_param: self.uid})
222222
return res["project"]["reviewMetrics"]["labelAggregate"]["count"]
223223

224+
def setup_from_ontology(self, ontology):
225+
"""
226+
* An ontology is a specific kind of a labeling frontend option that is compatible with the Editor.
227+
* Most customers use this interface
228+
"""
229+
frontend = list(
230+
client.get_labeling_frontends(
231+
where=LabelingFrontend.name == "Editor"))[0]
232+
return self.setup(frontend, ontology.normalized)
233+
224234
def setup(self, labeling_frontend, labeling_frontend_options):
225235
""" Finalizes the Project setup.
226236

tests/integration/test_client_errors.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
from multiprocessing.dummy import Pool
22
import os
33
import time
4-
54
import pytest
65

7-
from labelbox import Project, Dataset, User
6+
from labelbox import Project, Dataset, User, LabelingFrontend
87
import labelbox.client
98
import labelbox.exceptions
109

@@ -111,22 +110,26 @@ def test_invalid_attribute_error(client, rand_gen):
111110
project.delete()
112111

113112

114-
@pytest.mark.slow
115-
# TODO improve consistency
116-
@pytest.mark.skip(reason="Inconsistent test")
117-
def test_api_limit_error(client, rand_gen):
118-
project_id = client.create_project(name=rand_gen(str)).uid
113+
@pytest.mark.skip("timeouts cause failure before rate limit")
114+
def test_api_limit_error(client):
115+
global limited
116+
limited = False
119117

120118
def get(arg):
121119
try:
122-
return client.get_project(project_id)
120+
return client.get_user()
123121
except labelbox.exceptions.ApiLimitError as e:
124122
return e
125123

126-
with Pool(300) as pool:
127-
results = pool.map(get, list(range(2000)))
124+
n = 1600
125+
with Pool(30) as pool:
126+
start = time.time()
127+
results = list(pool.imap(get, range(n)), total=n)
128+
elapsed = time.time() - start
128129

130+
assert elapsed < 60, "Didn't finish fast enough"
129131
assert labelbox.exceptions.ApiLimitError in {type(r) for r in results}
132+
del limited
130133

131134
# Sleep at the end of this test to allow other tests to execute.
132-
time.sleep(60)
135+
#time.sleep(60)

tests/integration/test_data_upload.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@
22
import requests
33

44

5-
# TODO it seems that at some point Google Storage (gs prefix) started being
6-
# returned, and we can't just download those with requests. Fix this
7-
@pytest.mark.skip
8-
def test_file_upload(client, rand_gen):
5+
def test_file_upload(client, rand_gen, dataset):
96
data = rand_gen(str)
10-
url = client.upload_data(data.encode())
11-
assert requests.get(url).text == data
7+
uri = client.upload_data(data.encode())
8+
data_row = dataset.create_data_row(row_data=uri)
9+
assert requests.get(data_row.row_data).text == data

tests/integration/test_label.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from labelbox.schema.labeling_frontend import LabelingFrontend
12
import time
23

34
import pytest
@@ -30,18 +31,22 @@ def test_labels(label_pack):
3031
assert list(data_row.labels()) == []
3132

3233

33-
# TODO check if this is supported or not
34-
@pytest.mark.skip
35-
def test_label_export(label_pack):
34+
def test_label_export(client, label_pack):
3635
project, dataset, data_row, label = label_pack
37-
project.create_label(data_row=data_row, label="l2")
38-
39-
exported_labels_url = project.export_labels(5)
36+
#Old create_label works even with projects setup using the new editor.
37+
#It will appear in the export, just not in the new editor
38+
project.create_label(data_row=data_row, label="export_label")
39+
#Project has to be setup for export to be possible
40+
editor = list(
41+
client.get_labeling_frontends(
42+
where=LabelingFrontend.name == "editor"))[0]
43+
empty_ontology = {"tools": [], "classifications": []}
44+
project.setup(editor, empty_ontology)
45+
exported_labels_url = project.export_labels()
4046
assert exported_labels_url is not None
41-
if exported_labels_url is not None:
42-
exported_labels = requests.get(exported_labels_url)
43-
# TODO check content
44-
assert False
47+
exported_labels = requests.get(exported_labels_url)
48+
labels = [example['Label'] for example in exported_labels.json()]
49+
assert 'export_label' in labels
4550

4651

4752
def test_label_update(label_pack):

0 commit comments

Comments
 (0)