Skip to content

Commit f0dea6e

Browse files
Improve API documentation
1 parent 22ff727 commit f0dea6e

File tree

5 files changed

+137
-55
lines changed

5 files changed

+137
-55
lines changed

labelbox/client.py

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@
2121

2222

2323
class Client:
24-
""" A Labelbox client. Containes info necessary for connecting to
25-
the server (URL, authentication key). Provides functions for querying
26-
and creating top-level data objects (Projects, Datasets).
24+
""" A Labelbox client. Contains info necessary for connecting to
25+
a Labelbox server (URL, authentication key). Provides functions for
26+
querying and creating top-level data objects (Projects, Datasets).
2727
"""
2828

2929
def __init__(self, api_key=None,
@@ -34,6 +34,10 @@ def __init__(self, api_key=None,
3434
api_key (str): API key. If None, the key is obtained from
3535
the "LABELBOX_API_KEY" environment variable.
3636
endpoint (str): URL of the Labelbox server to connect to.
37+
Raises:
38+
labelbox.exceptions.AuthenticationError: If no `api_key`
39+
is provided as an argument or via the environment
40+
variable.
3741
"""
3842
if api_key is None:
3943
if _LABELBOX_API_KEY not in os.environ:
@@ -198,7 +202,7 @@ def upload_data(self, data):
198202

199203
return file_data["uploadFile"]["url"]
200204

201-
def get_single(self, db_object_type, uid):
205+
def _get_single(self, db_object_type, uid):
202206
""" Fetches a single object of the given type, for the given ID.
203207
204208
Args:
@@ -222,22 +226,38 @@ def get_single(self, db_object_type, uid):
222226
return db_object_type(self, res)
223227

224228
def get_project(self, project_id):
225-
""" Convenience for `client.get_single(Project, project_id)`. """
226-
return self.get_single(Project, project_id)
229+
""" Gets a single Project with the given ID.
230+
Args:
231+
project_id (str): Unique ID of the Project.
232+
Return:
233+
The sought Project.
234+
Raises:
235+
labelbox.exceptions.ResourceNotFoundError: If there is no
236+
Project with the given ID.
237+
"""
238+
return self._get_single(Project, project_id)
227239

228240
def get_dataset(self, dataset_id):
229-
""" Convenience for `client.get_single(Dataset, dataset_id)`. """
230-
return self.get_single(Dataset, dataset_id)
241+
""" Gets a single Dataset with the given ID.
242+
Args:
243+
dataset_id (str): Unique ID of the Dataset.
244+
Return:
245+
The sought Dataset.
246+
Raises:
247+
labelbox.exceptions.ResourceNotFoundError: If there is no
248+
Dataset with the given ID.
249+
"""
250+
return self._get_single(Dataset, dataset_id)
231251

232252
def get_user(self):
233-
""" Gets the current user database object. """
234-
return self.get_single(User, None)
253+
""" Gets the current User database object. """
254+
return self._get_single(User, None)
235255

236256
def get_organization(self):
237-
""" Gets the organization DB object of the current user. """
238-
return self.get_single(Organization, None)
257+
""" Gets the Organization DB object of the current user. """
258+
return self._get_single(Organization, None)
239259

240-
def get_all(self, db_object_type, where):
260+
def _get_all(self, db_object_type, where):
241261
""" Fetches all the objects of the given type the user has access to.
242262
243263
Args:
@@ -270,7 +290,7 @@ def get_projects(self, where=None):
270290
labelbox.exceptions.LabelboxError: Any error raised by
271291
`Client.execute` can also be raised by this function.
272292
"""
273-
return self.get_all(Project, where)
293+
return self._get_all(Project, where)
274294

275295
def get_datasets(self, where=None):
276296
""" Fetches all the datasets the user has access to.
@@ -284,7 +304,7 @@ def get_datasets(self, where=None):
284304
labelbox.exceptions.LabelboxError: Any error raised by
285305
`Client.execute` can also be raised by this function.
286306
"""
287-
return self.get_all(Dataset, where)
307+
return self._get_all(Dataset, where)
288308

289309
def get_labeling_frontends(self, where=None):
290310
""" Fetches all the labeling frontends.
@@ -298,7 +318,7 @@ def get_labeling_frontends(self, where=None):
298318
labelbox.exceptions.LabelboxError: Any error raised by
299319
`Client.execute` can also be raised by this function.
300320
"""
301-
return self.get_all(LabelingFrontend, where)
321+
return self._get_all(LabelingFrontend, where)
302322

303323
def _create(self, db_object_type, data):
304324
""" Creates a object on the server. Attribute values are
@@ -328,7 +348,9 @@ def _create(self, db_object_type, data):
328348
def create_dataset(self, **kwargs):
329349
""" Creates a Dataset object on the server. Attribute values are
330350
passed as keyword arguments:
331-
>>> dataset = client.create_dataset(name="MyDataset")
351+
>>> project = client.get_project("uid_of_my_project")
352+
>>> dataset = client.create_dataset(name="MyDataset",
353+
>>> projects=project)
332354
333355
Kwargs:
334356
Keyword arguments with new Dataset attribute values.
@@ -338,7 +360,7 @@ def create_dataset(self, **kwargs):
338360
a new Dataset object.
339361
Raises:
340362
InvalidAttributeError: in case the Dataset type does not contain
341-
any of the field names given in kwargs.
363+
any of the attribute names given in kwargs.
342364
"""
343365
return self._create(Dataset, kwargs)
344366

@@ -355,6 +377,6 @@ def create_project(self, **kwargs):
355377
a new Project object.
356378
Raises:
357379
InvalidAttributeError: in case the Project type does not contain
358-
any of the field names given in kwargs.
380+
any of the attribute names given in kwargs.
359381
"""
360382
return self._create(Project, kwargs)

labelbox/orm/db_object.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def _set_field_values(self, field_values):
6868

6969
def __repr__(self):
7070
type_name = self.type_name()
71-
if "uid" in dir(self):
71+
if "uid" in self.__dict__:
7272
return "<%s ID: %s>" % (type_name, self.uid)
7373
else:
7474
return "<%s>" % type_name
@@ -217,7 +217,7 @@ class BulkDeletable:
217217
type.
218218
"""
219219
@staticmethod
220-
def bulk_delete(objects, use_where_clause):
220+
def _bulk_delete(objects, use_where_clause):
221221
"""
222222
Args:
223223
objects (list): Objects to delete. All objects must be of the same

labelbox/orm/model.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,12 @@ def __init__(self, relationship_type, destination_type_name,
196196
def destination_type(self):
197197
return Entity.named(self.destination_type_name)
198198

199+
def __str__(self):
200+
return self.name
201+
202+
def __repr__(self):
203+
return "<Relationship: %r>" % self.name
204+
199205

200206
class Entity:
201207
""" An entity that contains fields and relationships. """

labelbox/schema.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -346,8 +346,12 @@ class DataRow(DbObject, Updateable, BulkDeletable):
346346
metadata = Relationship.ToMany("AssetMetadata", False, "metadata")
347347

348348
@staticmethod
349-
def bulk_delete(objects):
350-
BulkDeletable.bulk_delete(objects, True)
349+
def bulk_delete(data_rows):
350+
""" Deletes all the given DataRows.
351+
Args:
352+
data_rows (list of DataRow): The DataRows to delete.
353+
"""
354+
BulkDeletable._bulk_delete(data_rows, True)
351355

352356
def __init__(self, *args, **kwargs):
353357
super().__init__(*args, **kwargs)
@@ -387,8 +391,12 @@ def __init__(self, *args, **kwargs):
387391
reviews = Relationship.ToMany("Review", False)
388392

389393
@staticmethod
390-
def bulk_delete(objects):
391-
BulkDeletable.bulk_delete(objects, False)
394+
def bulk_delete(labels):
395+
""" Deletes all the given Labels.
396+
Args:
397+
labels (list of Label): The Labels to delete.
398+
"""
399+
BulkDeletable._bulk_delete(labels, False)
392400

393401
def create_review(self, **kwargs):
394402
""" Creates a Review for this label.
@@ -570,6 +578,21 @@ class Webhook(DbObject):
570578

571579
@staticmethod
572580
def create(client, topics, url, secret, project):
581+
""" Creates a Webhook.
582+
Args:
583+
client (Client): The Labelbox client used to connect
584+
to the server.
585+
topics (list of str): A list of topics this Webhook should
586+
get notifications for.
587+
url (str): The URL to which notifications should be sent
588+
by the Labelbox server.
589+
secret (str): A secret key used for signing notifications.
590+
project (Project or None): The project for which notifications
591+
should be sent. If None notifications are sent for all
592+
events in your organization.
593+
Return:
594+
A newly created Webhook.
595+
"""
573596
query_str, params = query.create_webhook(topics, url, secret, project)
574597
res = client.execute(query_str, params)
575598
return Webhook(client, res["data"]["createWebhook"])

tools/db_object_doc_gen.py

Lines changed: 61 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def header(level, text):
8585

8686

8787
def paragraph(text):
88-
return tag(text, "p")
88+
return tag(inject_class_links(text), "p")
8989

9090

9191
def strong(text):
@@ -121,15 +121,15 @@ def header_link(text, header_id):
121121
return tag(text, "a", {"href":"#" + header_id})
122122

123123

124-
def class_link(cls):
124+
def class_link(cls, text):
125125
""" Generates an intra-document link for the given class. Example:
126126
>>> from labelbox import Project
127-
>>> class_link(Project)
128-
>>> <a href="#class_labelbox_schema_project">Project</a>
127+
>>> class_link(Project, "blah")
128+
>>> <a href="#class_labelbox_schema_project">blah</a>
129129
"""
130130
header_id = "class_" + labelbox.utils.snake_case(qual_class_name(cls).
131131
replace(".", "_"))
132-
return header_link(cls.__name__, header_id)
132+
return header_link(text, header_id)
133133

134134

135135
def inject_class_links(text):
@@ -141,15 +141,15 @@ def inject_class_links(text):
141141
matches = list(re.finditer(pattern, text))
142142
for match in reversed(matches):
143143
start, end = match.span()
144-
text = text[:start] + class_link(cls) + text[end:]
144+
text = text[:start] + class_link(cls, match.group()) + text[end:]
145145
return text
146146

147147

148148
def is_method(attribute):
149149
""" Determines if the given attribute is most likely a method. It's
150150
approximative since from Python 3 there are no more unbound methods. """
151151
return inspect.isfunction(attribute) and "." in attribute.__qualname__ \
152-
and inspect.getfullargspec(attribute).args[0] == 'self'
152+
and inspect.getfullargspec(attribute).args[:1] == ['self']
153153

154154

155155
def preprocess_docstring(docstring):
@@ -230,26 +230,51 @@ def process(collection, f):
230230

231231
return "".join(result)
232232

233+
def parse_maybe_block(text):
234+
""" Adapts to text. Calls `parse_block` if there is a codeblock
235+
indented, otherwise just joins lines into a single line and
236+
reduces whitespace.
237+
"""
238+
if text is None:
239+
return ""
240+
if re.findall(r"\n\s+>>>", text):
241+
return parse_block()
242+
return re.sub(r"\s+", " ", text).strip()
243+
233244
parts = (("Args: ", parse_list(args)),
234-
("Kwargs: ", parse_block(kwargs)),
235-
("Returns: ", parse_block(returns)),
245+
("Kwargs: ", parse_maybe_block(kwargs)),
246+
("Returns: ", parse_maybe_block(returns)),
236247
("Raises: ", parse_list(raises)))
237248

238249
return parse_block(docstring) + unordered_list([
239250
strong(name) + item for name, item in parts if bool(item)])
240251

241252

242-
def generate_methods(cls):
243-
""" Generates HelpDocs style documentation for all the methods
244-
of the given class.
253+
def generate_functions(cls, predicate):
254+
""" Generates HelpDocs style documentation for the functions
255+
of the given class that satisfy the given predicate. The functions
256+
also must not being with "_", with the exception of Client.__init__.
257+
258+
Args:
259+
cls (type): The class being generated.
260+
predicate (callable): A callable accepting a single argument
261+
(class attribute) and returning a bool indicating if
262+
that attribute should be included in documentation
263+
generation.
264+
Return:
265+
Textual documentation of functions belonging to the given
266+
class that satisfy the given predicate.
245267
"""
246268
text = []
247-
for attr_name in dir(cls):
248-
attr = getattr(cls, attr_name)
249-
if ((is_method(attr) and not attr_name.startswith("_")) or
250-
(cls == labelbox.Client and attr_name == "__init__")):
251-
text.append(paragraph(generate_signature(attr)))
252-
text.append(preprocess_docstring(attr.__doc__))
269+
for name, attr in cls.__dict__.items():
270+
if predicate(attr):
271+
# static and class methods gave the __func__ attribute
272+
# with the original definition that we need.
273+
attr = getattr(attr, "__func__", attr)
274+
if not name.startswith("_") or (cls == labelbox.Client and
275+
name == "__init__"):
276+
text.append(paragraph(generate_signature(attr)))
277+
text.append(preprocess_docstring(attr.__doc__))
253278

254279
return "".join(text)
255280

@@ -319,29 +344,35 @@ def generate_class(cls, schema_class):
319344
text.append(generate_fields(cls))
320345
text.append(header(3, "Relationships"))
321346
text.append(generate_relationships(cls))
322-
methods = generate_methods(cls).strip()
323-
if len(methods):
324-
text.append(header(3, "Methods"))
325-
text.append(methods)
347+
348+
for name, predicate in (
349+
("Static Methods", lambda attr: type(attr) == staticmethod),
350+
("Class Methods", lambda attr: type(attr) == classmethod),
351+
("Object Methods", is_method)):
352+
functions = generate_functions(cls, predicate).strip()
353+
if len(functions):
354+
text.append(header(3, name))
355+
text.append(functions)
356+
326357
return "\n".join(text)
327358

328359

329-
def generate_all(general_classes, schema_classes, error_classes):
360+
def generate_all():
330361
""" Generates the full HelpDocs API documentation article body. """
331362
text = []
332363
text.append(header(3, "General Classes"))
333-
text.append(unordered_list([qual_class_name(cls) for cls in general_classes]))
364+
text.append(unordered_list([qual_class_name(cls) for cls in GENERAL_CLASSES]))
334365
text.append(header(3, "Data Classes"))
335-
text.append(unordered_list([qual_class_name(cls) for cls in schema_classes]))
366+
text.append(unordered_list([qual_class_name(cls) for cls in SCHEMA_CLASSES]))
336367
text.append(header(3, "Error Classes"))
337-
text.append(unordered_list([qual_class_name(cls) for cls in error_classes]))
368+
text.append(unordered_list([qual_class_name(cls) for cls in ERROR_CLASSES]))
338369

339370
text.append(header(1, "General classes"))
340-
text.extend(generate_class(cls, False) for cls in general_classes)
371+
text.extend(generate_class(cls, False) for cls in GENERAL_CLASSES)
341372
text.append(header(1, "Data Classes"))
342-
text.extend(generate_class(cls, True) for cls in schema_classes)
373+
text.extend(generate_class(cls, True) for cls in SCHEMA_CLASSES)
343374
text.append(header(1, "Error Classes"))
344-
text.extend(generate_class(cls, False) for cls in error_classes)
375+
text.extend(generate_class(cls, False) for cls in ERROR_CLASSES)
345376
return "\n".join(text)
346377

347378

@@ -353,7 +384,7 @@ def main():
353384

354385
args = argp.parse_args()
355386

356-
body = generate_all(GENERAL_CLASSES, SCHEMA_CLASSES, ERROR_CLASSES)
387+
body = generate_all()
357388

358389
if args.helpdocs_api_key is not None:
359390
url = "https://api.helpdocs.io/v1/article/zg9hp7yx3u?key=" + \

0 commit comments

Comments
 (0)