Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Triggers a pypi publication when a release is created

name: Publish Python Package

on:
release:
types: [created]

jobs:
deploy:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2

- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.x'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine

- name: Build and publish
env:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: |
python setup.py sdist bdist_wheel
twine upload dist/*

- name: Update help docs
run: |
python setup.py install
python ./tools/api_reference_generator.py ${{ secrets.HELPDOCS_API_KEY }}
58 changes: 58 additions & 0 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Labelbox Python SDK

on:
push:
branches: [ develop ]
pull_request:
branches: [ develop ]

jobs:
build:
if: github.event.pull_request.head.repo.full_name == github.repository

runs-on: ubuntu-latest
strategy:
max-parallel: 1
matrix:
python-version: [3.6, 3.7]

steps:
- uses: actions/checkout@v2
with:
token: ${{ secrets.ACTIONS_ACCESS_TOKEN }}
ref: ${{ github.head_ref }}

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}

- name: yapf
id: yapf
uses: AlexanderMelde/yapf-action@master
with:
args: --verbose --recursive --parallel --style "google"

- name: install labelbox package
run: |
python setup.py install
- name: mypy
run: |
python -m pip install --upgrade pip
pip install mypy==0.782
mypy -p labelbox --pretty --show-error-codes
- name: Install package and test dependencies
run: |
pip install tox==3.18.1 tox-gh-actions==1.3.0

- name: Test with tox
env:
# make sure to tell tox to use these environs in tox.ini
LABELBOX_TEST_API_KEY: ${{ secrets.LABELBOX_API_KEY }}
LABELBOX_TEST_ENDPOINT: "https://api.labelbox.com/graphql"
# TODO: create a staging environment (develop)
# we only test against prod right now because the merges are right into
# the main branch which is develop right now
LABELBOX_TEST_ENVIRON: "PROD"
run: |
tox -- -svv
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## Version 2.4.3 (2020-08-04)

### Added
* `BulkImportRequest` data type

## Version 2.4.2 (2020-08-01)
### Fixed
* `Client.upload_data` will now pass the correct `content-length` when uploading data.

## Version 2.4.1 (2020-07-22)
### Fixed
* `Dataset.create_data_row` and `Dataset.create_data_rows` will now upload with content type to ensure the Labelbox editor can show videos.
Expand Down
19 changes: 10 additions & 9 deletions CONTRIB.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,13 @@ Each release should follow the following steps:
2. Make sure the `CHANGELOG.md` contains appropriate info
3. Commit these changes and tag the commit in Git as `vX.Y`
4. Merge `develop` to `master` (fast-forward only).
5. Generate a GitHub release.
6. Build the library in the [standard
way](https://packaging.python.org/tutorials/packaging-projects/#generating-distribution-archives)
7. Upload the distribution archives in the [standard
way](https://packaging.python.org/tutorials/packaging-projects/#uploading-the-distribution-archives).
You will need credentials for the `labelbox` PyPI user.
8. Run the `REPO_ROOT/tools/api_reference_generator.py` script to update
[HelpDocs documentation](https://labelbox.helpdocs.io/docs/). You will need
to provide a HelpDocs API key for.
5. Create a GitHub release.
6. This will kick off a Github Actions workflow that will:
- Build the library in the [standard
way](https://packaging.python.org/tutorials/packaging-projects/#generating-distribution-archives)
- Upload the distribution archives in the [standard
way](https://packaging.python.org/tutorials/packaging-projects/#uploading-the-distribution-archives)
with credentials for the `labelbox` PyPI user.
- Run the `REPO_ROOT/tools/api_reference_generator.py` script to update
[HelpDocs documentation](https://labelbox.helpdocs.io/docs/). You will need
to provide a HelpDocs API key for.
94 changes: 60 additions & 34 deletions labelbox/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import logging
import mimetypes
import os
from typing import Tuple

import requests
import requests.exceptions
Expand All @@ -18,10 +19,8 @@
from labelbox.schema.organization import Organization
from labelbox.schema.labeling_frontend import LabelingFrontend


logger = logging.getLogger(__name__)


_LABELBOX_API_KEY = "LABELBOX_API_KEY"


Expand All @@ -31,7 +30,8 @@ class Client:
querying and creating top-level data objects (Projects, Datasets).
"""

def __init__(self, api_key=None,
def __init__(self,
api_key=None,
endpoint='https://api.labelbox.com/graphql'):
""" Creates and initializes a Labelbox Client.

Expand All @@ -54,9 +54,11 @@ def __init__(self, api_key=None,
logger.info("Initializing Labelbox client at '%s'", endpoint)

self.endpoint = endpoint
self.headers = {'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer %s' % api_key}
self.headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer %s' % api_key
}

def execute(self, query, params=None, timeout=10.0):
""" Sends a request to the server for the execution of the
Expand Down Expand Up @@ -95,15 +97,17 @@ def convert_value(value):
return value

if params is not None:
params = {key: convert_value(value) for key, value in params.items()}
params = {
key: convert_value(value) for key, value in params.items()
}

data = json.dumps(
{'query': query, 'variables': params}).encode('utf-8')
data = json.dumps({'query': query, 'variables': params}).encode('utf-8')

try:
response = requests.post(self.endpoint, data=data,
headers=self.headers,
timeout=timeout)
response = requests.post(self.endpoint,
data=data,
headers=self.headers,
timeout=timeout)
logger.debug("Response: %s", response.text)
except requests.exceptions.Timeout as e:
raise labelbox.exceptions.TimeoutError(str(e))
Expand Down Expand Up @@ -136,8 +140,8 @@ def check_errors(keywords, *path):
return error
return None

if check_errors(["AUTHENTICATION_ERROR"],
"extensions", "exception", "code") is not None:
if check_errors(["AUTHENTICATION_ERROR"], "extensions", "exception",
"code") is not None:
raise labelbox.exceptions.AuthenticationError("Invalid API key")

authorization_error = check_errors(["AUTHORIZATION_ERROR"],
Expand All @@ -155,7 +159,8 @@ def check_errors(keywords, *path):
else:
raise labelbox.exceptions.InvalidQueryError(message)

graphql_error = check_errors(["GRAPHQL_PARSE_FAILED"], "extensions", "code")
graphql_error = check_errors(["GRAPHQL_PARSE_FAILED"], "extensions",
"code")
if graphql_error is not None:
raise labelbox.exceptions.InvalidQueryError(
graphql_error["message"])
Expand All @@ -167,12 +172,12 @@ def check_errors(keywords, *path):

if len(errors) > 0:
logger.warning("Unparsed errors on query execution: %r", errors)
raise labelbox.exceptions.LabelboxError(
"Unknown error: %s" % str(errors))
raise labelbox.exceptions.LabelboxError("Unknown error: %s" %
str(errors))

return response["data"]

def upload_file(self, path):
def upload_file(self, path: str) -> str:
"""Uploads given path to local file.

Also includes best guess at the content type of the file.
Expand All @@ -186,39 +191,58 @@ def upload_file(self, path):

"""
content_type, _ = mimetypes.guess_type(path)
basename = os.path.basename(path)
filename = os.path.basename(path)
with open(path, "rb") as f:
return self.upload_data(data=(basename, f.read(), content_type))

def upload_data(self, data):
return self.upload_data(content=f.read(),
filename=filename,
content_type=content_type)

def upload_data(self,
content: bytes,
filename: str = None,
content_type: str = None) -> str:
""" Uploads the given data (bytes) to Labelbox.

Args:
data (bytes): The data to upload.
content: bytestring to upload
filename: name of the upload
content_type: content type of data uploaded

Returns:
str, the URL of uploaded data.

Raises:
labelbox.exceptions.LabelboxError: If upload failed.
"""

request_data = {
"operations": json.dumps({
"variables": {"file": None, "contentLength": len(data), "sign": False},
"query": """mutation UploadFile($file: Upload!, $contentLength: Int!,
"operations":
json.dumps({
"variables": {
"file": None,
"contentLength": len(content),
"sign": False
},
"query":
"""mutation UploadFile($file: Upload!, $contentLength: Int!,
$sign: Boolean) {
uploadFile(file: $file, contentLength: $contentLength,
sign: $sign) {url filename} } """,}),
sign: $sign) {url filename} } """,
}),
"map": (None, json.dumps({"1": ["variables.file"]})),
}
}
response = requests.post(
self.endpoint,
headers={"authorization": "Bearer %s" % self.api_key},
data=request_data,
files={"1": data}
)
files={
"1": (filename, content, content_type) if
(filename and content_type) else content
})

try:
file_data = response.json().get("data", None)
except ValueError as e: # response is not valid JSON
except ValueError as e: # response is not valid JSON
raise labelbox.exceptions.LabelboxError(
"Failed to upload, unknown cause", e)

Expand Down Expand Up @@ -350,9 +374,11 @@ def _create(self, db_object_type, data):
"""
# Convert string attribute names to Field or Relationship objects.
# Also convert Labelbox object values to their UIDs.
data = {db_object_type.attribute(attr) if isinstance(attr, str) else attr:
value.uid if isinstance(value, DbObject) else value
for attr, value in data.items()}
data = {
db_object_type.attribute(attr) if isinstance(attr, str) else attr:
value.uid if isinstance(value, DbObject) else value
for attr, value in data.items()
}

query_string, params = query.create(db_object_type, data)
res = self.execute(query_string, params)
Expand Down
16 changes: 12 additions & 4 deletions labelbox/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
class LabelboxError(Exception):
"""Base class for exceptions."""

def __init__(self, message, cause=None):
"""
Args:
Expand Down Expand Up @@ -34,8 +35,8 @@ def __init__(self, db_object_type, params):
db_object_type (type): A labelbox.schema.DbObject subtype.
params (dict): Dict of params identifying the sought resource.
"""
super().__init__("Resouce '%s' not found for params: %r" % (
db_object_type.type_name(), params))
super().__init__("Resouce '%s' not found for params: %r" %
(db_object_type.type_name(), params))
self.db_object_type = db_object_type
self.params = params

Expand All @@ -56,6 +57,7 @@ class InvalidQueryError(LabelboxError):

class NetworkError(LabelboxError):
"""Raised when an HTTPError occurs."""

def __init__(self, cause):
super().__init__(str(cause), cause)
self.cause = cause
Expand All @@ -69,9 +71,10 @@ class TimeoutError(LabelboxError):
class InvalidAttributeError(LabelboxError):
""" Raised when a field (name or Field instance) is not valid or found
for a specific DB object type. """

def __init__(self, db_object_type, field):
super().__init__("Field(s) '%r' not valid on DB type '%s'" % (
field, db_object_type.type_name()))
super().__init__("Field(s) '%r' not valid on DB type '%s'" %
(field, db_object_type.type_name()))
self.db_object_type = db_object_type
self.field = field

Expand All @@ -80,3 +83,8 @@ class ApiLimitError(LabelboxError):
""" Raised when the user performs too many requests in a short period
of time. """
pass


class MalformedQueryException(Exception):
""" Raised when the user submits a malformed query."""
pass
5 changes: 2 additions & 3 deletions labelbox/orm/comparison.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
from enum import Enum, auto


""" Classes for defining the client-side comparison operations used
for filtering data in fetches. Intended for use by library internals
and not by the end user.
Expand Down Expand Up @@ -60,7 +58,8 @@ def __eq__(self, other):
(self.first == other.second and self.second == other.first))

def __hash__(self):
return hash(self.op) + 2833 * hash(self.first) + 2837 * hash(self.second)
return hash(
self.op) + 2833 * hash(self.first) + 2837 * hash(self.second)

def __repr__(self):
return "%r %s %r" % (self.first, self.op.name, self.second)
Expand Down
Loading