Skip to content

Commit 533570a

Browse files
rllinflorijanstamenkovicTohnJhomasalexandracotaAlexandra Cota
authored
merge develop to master (#29)
* Fix api_reference_generator deletion. * [fix] Project setup order of operations (#24) * Improve Exception handling. * Fix LabelboxError.__str__ * Add 30 min flag to export_labels. (#22) Co-authored-by: Alexandra Cota <alexandracota@Alexandras-MBP.localdomain> * Add CONTRIB.md (#26) * [BACKEND-766] upload with content type guess (#28) * wip * clean up * change log and bump version Co-authored-by: Florijan Stamenkovic <florijan.stamenkovic@gmail.com> Co-authored-by: TohnJhomas <49878111+TohnJhomas@users.noreply.github.com> Co-authored-by: Alex Cota <cota.alexandra14@gmail.com> Co-authored-by: Alexandra Cota <alexandracota@Alexandras-MBP.localdomain> Co-authored-by: Florijan Stamenković <florijan@toptal.com>
1 parent ffc259f commit 533570a

File tree

9 files changed

+567
-27
lines changed

9 files changed

+567
-27
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## Version 2.4.1 (2020-07-22)
4+
### Fixed
5+
* `Dataset.create_data_row` and `Dataset.create_data_rows` will now upload with content type to ensure the Labelbox editor can show videos.
6+
37
## Version 2.4 (2020-01-30)
48

59
### Added

CONTRIB.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Labelbox Python SDK Contribution Guide
2+
3+
## Repository Organization
4+
5+
The SDK source (excluding tests and support tools) is organized into the
6+
following packages/modules:
7+
* `orm/` package contains code that supports the general mapping of Labelbox
8+
data to Python objects. This includes base classes, attribute (field and
9+
relationship) classes, generic GraphQL queries etc.
10+
* `schema/` package contains definitions of classes which represent data type
11+
(e.g. Project, Label etc.). It relies on `orm/` classes for easy and succinct
12+
object definitions. It also contains custom functionalities and custom GraphQL
13+
templates where necessary.
14+
* `client.py` contains the `Client` class that's the client-side stub for
15+
communicating with Labelbox servers.
16+
* `exceptions.py` contains declarations for all Labelbox errors.
17+
* `pagination.py` contains support for paginated relationship and collection
18+
fetching.
19+
* `utils.py` contains utility functions.
20+
21+
## Branches
22+
23+
* All development happens in per-feature branches prefixed by contributor's
24+
initials. For example `fs/feature_name`.
25+
* Approved PRs are merged to the `develop` branch.
26+
* The `develop` branch is merged to `master` on each release.
27+
28+
## Testing
29+
30+
Currently the SDK functionality is tested using integration tests. These tests
31+
communicate with a Labelbox server (by default the staging server) and are in
32+
that sense not self-contained. Besides that they are organized like unit test
33+
and are based on the `pytest` library.
34+
35+
To execute tests you will need to provide an API key for the server you're using
36+
for testing (staging by default) in the `LABELBOX_TEST_API_KEY` environment
37+
variable. For more info see [Labelbox API key
38+
docs](https://labelbox.helpdocs.io/docs/api/getting-started).
39+
40+
## Release Steps
41+
42+
Each release should follow the following steps:
43+
44+
1. Update the Python SDK package version in `REPO_ROOT/setup.py`
45+
2. Make sure the `CHANGELOG.md` contains appropriate info
46+
3. Commit these changes and tag the commit in Git as `vX.Y`
47+
4. Merge `develop` to `master` (fast-forward only).
48+
5. Generate a GitHub release.
49+
6. Build the library in the [standard
50+
way](https://packaging.python.org/tutorials/packaging-projects/#generating-distribution-archives)
51+
7. Upload the distribution archives in the [standard
52+
way](https://packaging.python.org/tutorials/packaging-projects/#uploading-the-distribution-archives).
53+
You will need credentials for the `labelbox` PyPI user.
54+
8. Run the `REPO_ROOT/tools/api_reference_generator.py` script to update
55+
[HelpDocs documentation](https://labelbox.helpdocs.io/docs/). You will need
56+
to provide a HelpDocs API key for.

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Labelbox Python API
1+
# Labelbox Python SDK
22

33
Labelbox is the enterprise-grade training data solution with fast AI enabled labeling tools, labeling automation, human workforce, data management, a powerful API for integration & SDK for extensibility. Visit http://labelbox.com/ for more information.
44

@@ -29,3 +29,6 @@ client = Client()
2929
## Documentation
3030

3131
[Visit our docs](https://labelbox.com/docs/python-api) to learn how to [create a project](https://labelbox.com/docs/python-api/create-first-project), read through some helpful user guides, and view our [API reference](https://labelbox.com/docs/python-api/api-reference).
32+
33+
## Repo Organization and Contribution
34+
Please consult `CONTRIB.md`

labelbox/client.py

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from datetime import datetime, timezone
22
import json
33
import logging
4+
import mimetypes
45
import os
56

67
import requests
@@ -75,7 +76,7 @@ def execute(self, query, params=None, timeout=10.0):
7576
labelbox.exceptions.InvalidQueryError: If `query` is not
7677
syntactically or semantically valid (checked server-side).
7778
labelbox.exceptions.ApiLimitError: If the server API limit was
78-
exceeded. See "How to import data" in the online documentation
79+
exceeded. See "How to import data" in the online documentation
7980
to see API limits.
8081
labelbox.exceptions.TimeoutError: If response was not received
8182
in `timeout` seconds.
@@ -112,14 +113,14 @@ def convert_value(value):
112113
raise labelbox.exceptions.NetworkError(e)
113114

114115
except Exception as e:
115-
logger.error("Unknown error: %s", str(e))
116-
raise labelbox.exceptions.LabelboxError(str(e))
116+
raise labelbox.exceptions.LabelboxError(
117+
"Unknown error during Client.query(): " + str(e), e)
117118

118119
try:
119120
response = response.json()
120121
except:
121122
raise labelbox.exceptions.LabelboxError(
122-
"Failed to parse response as JSON: %s", response.text)
123+
"Failed to parse response as JSON: %s" % response.text)
123124

124125
errors = response.get("errors", [])
125126

@@ -171,9 +172,27 @@ def check_errors(keywords, *path):
171172

172173
return response["data"]
173174

175+
def upload_file(self, path):
176+
"""Uploads given path to local file.
177+
178+
Also includes best guess at the content type of the file.
179+
180+
Args:
181+
path (str): path to local file to be uploaded.
182+
Returns:
183+
str, the URL of uploaded data.
184+
Raises:
185+
labelbox.exceptions.LabelboxError: If upload failed.
186+
187+
"""
188+
content_type, _ = mimetypes.guess_type(path)
189+
basename = os.path.basename(path)
190+
with open(path, "rb") as f:
191+
return self.upload_data(data=(basename, f.read(), content_type))
192+
174193
def upload_data(self, data):
175194
""" Uploads the given data (bytes) to Labelbox.
176-
195+
177196
Args:
178197
data (bytes): The data to upload.
179198
Returns:
@@ -183,8 +202,8 @@ def upload_data(self, data):
183202
"""
184203
request_data = {
185204
"operations": json.dumps({
186-
"variables": {"file": None, "contentLength": len(data), "sign": False},
187-
"query": """mutation UploadFile($file: Upload!, $contentLength: Int!,
205+
"variables": {"file": None, "contentLength": len(data), "sign": False},
206+
"query": """mutation UploadFile($file: Upload!, $contentLength: Int!,
188207
$sign: Boolean) {
189208
uploadFile(file: $file, contentLength: $contentLength,
190209
sign: $sign) {url filename} } """,}),
@@ -199,9 +218,9 @@ def upload_data(self, data):
199218

200219
try:
201220
file_data = response.json().get("data", None)
202-
except ValueError: # response is not valid JSON
221+
except ValueError as e: # response is not valid JSON
203222
raise labelbox.exceptions.LabelboxError(
204-
"Failed to upload, unknown cause")
223+
"Failed to upload, unknown cause", e)
205224

206225
if not file_data or not file_data.get("uploadFile", None):
207226
raise labelbox.exceptions.LabelboxError(

labelbox/exceptions.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
class LabelboxError(Exception):
22
"""Base class for exceptions."""
3-
def __init__(self, message, *args):
4-
super().__init__(*args)
3+
def __init__(self, message, cause=None):
4+
"""
5+
Args:
6+
message (str): Informative message about the exception.
7+
cause (Exception): The cause of the exception (an Exception
8+
raised by Python or another library). Optional.
9+
"""
10+
super().__init__(message, cause)
511
self.message = message
12+
self.cause = cause
13+
14+
def __str__(self):
15+
return self.message + str(self.args)
616

717

818
class AuthenticationError(LabelboxError):
@@ -31,9 +41,8 @@ def __init__(self, db_object_type, params):
3141

3242

3343
class ValidationFailedError(LabelboxError):
34-
"""Exception raised for when a GraphQL query fails validation (query cost, etc.)
35-
36-
E.g. a query that is too expensive, or depth is too deep.
44+
"""Exception raised for when a GraphQL query fails validation (query cost,
45+
etc.) E.g. a query that is too expensive, or depth is too deep.
3746
"""
3847
pass
3948

@@ -47,10 +56,8 @@ class InvalidQueryError(LabelboxError):
4756

4857
class NetworkError(LabelboxError):
4958
"""Raised when an HTTPError occurs."""
50-
def __init__(self, cause, message=None):
51-
if message is None:
52-
message = str(cause)
53-
super().__init__(message)
59+
def __init__(self, cause):
60+
super().__init__(str(cause), cause)
5461
self.cause = cause
5562

5663

labelbox/schema/dataset.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,15 @@ def create_data_row(self, **kwargs):
4848
# If row data is a local file path, upload it to server.
4949
row_data = kwargs[DataRow.row_data.name]
5050
if os.path.exists(row_data):
51-
with open(row_data, "rb") as f:
52-
kwargs[DataRow.row_data.name] = self.client.upload_data(f.read())
51+
kwargs[DataRow.row_data.name] = self.client.upload_file(row_data)
5352

5453
kwargs[DataRow.dataset.name] = self
5554

5655
return self.client._create(DataRow, kwargs)
5756

5857
def create_data_rows(self, items):
5958
""" Creates multiple DataRow objects based on the given `items`.
60-
59+
6160
Each element in `items` can be either a `str` or a `dict`. If
6261
it is a `str`, then it is interpreted as a local file path. The file
6362
is uploaded to Labelbox and a DataRow referencing it is created.
@@ -91,9 +90,7 @@ def create_data_rows(self, items):
9190

9291
def upload_if_necessary(item):
9392
if isinstance(item, str):
94-
with open(item, "rb") as f:
95-
item_data = f.read()
96-
item_url = self.client.upload_data(item_data)
93+
item_url = self.client.upload_file(item)
9794
# Convert item from str into a dict so it gets processed
9895
# like all other dicts.
9996
item = {DataRow.row_data: item_url,

labelbox/schema/project.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ def export_labels(self, timeout_seconds=60):
112112
""" Calls the server-side Label exporting that generates a JSON
113113
payload, and returns the URL to that payload.
114114
115+
Will only generate a new URL at a max frequency of 30 min.
116+
115117
Args:
116118
timeout_seconds (float): Max waiting time, in seconds.
117119
Returns:
@@ -199,14 +201,15 @@ def setup(self, labeling_frontend, labeling_frontend_options):
199201
if not isinstance(labeling_frontend_options, str):
200202
labeling_frontend_options = json.dumps(labeling_frontend_options)
201203

204+
self.labeling_frontend.connect(labeling_frontend)
205+
202206
LFO = Entity.LabelingFrontendOptions
203207
labeling_frontend_options = self.client._create(
204208
LFO, {LFO.project: self, LFO.labeling_frontend: labeling_frontend,
205209
LFO.customization_options: labeling_frontend_options,
206210
LFO.organization: organization
207211
})
208212

209-
self.labeling_frontend.connect(labeling_frontend)
210213
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
211214
self.update(setup_complete=timestamp)
212215

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
setuptools.setup(
99
name="labelbox",
10-
version="2.4",
10+
version="2.4.1",
1111
author="Labelbox",
1212
author_email="engineering@labelbox.com",
1313
description="Labelbox Python API",

0 commit comments

Comments
 (0)