Skip to content

Commit f0f8467

Browse files
committed
add methods to manage feature schemas
1 parent 24dd42a commit f0f8467

File tree

7 files changed

+350
-8
lines changed

7 files changed

+350
-8
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,5 @@ test-custom: build
4444
-e DA_GCP_LABELBOX_API_KEY=${DA_GCP_LABELBOX_API_KEY} \
4545
-e LABELBOX_TEST_API_KEY_CUSTOM=${LABELBOX_TEST_API_KEY_CUSTOM} \
4646
-e LABELBOX_TEST_GRAPHQL_API_ENDPOINT=${LABELBOX_TEST_GRAPHQL_API_ENDPOINT} \
47+
-e LABELBOX_TEST_REST_API_ENDPOINT=${LABELBOX_TEST_REST_API_ENDPOINT} \
4748
local/labelbox-python:test pytest $(PATH_TO_TEST)

labelbox/client.py

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import mimetypes
99
import os
1010
import time
11+
import urllib.parse
1112

1213
from google.api_core import retry
1314
import requests
@@ -55,7 +56,8 @@ def __init__(self,
5556
api_key=None,
5657
endpoint='https://api.labelbox.com/graphql',
5758
enable_experimental=False,
58-
app_url="https://app.labelbox.com"):
59+
app_url="https://app.labelbox.com",
60+
rest_endpoint="https://api.labelbox.com/api/v1"):
5961
""" Creates and initializes a Labelbox Client.
6062
6163
Logging is defaulted to level WARNING. To receive more verbose
@@ -88,6 +90,7 @@ def __init__(self,
8890
logger.info("Initializing Labelbox client at '%s'", endpoint)
8991
self.app_url = app_url
9092
self.endpoint = endpoint
93+
self.rest_endpoint = rest_endpoint
9194
self.headers = {
9295
'Accept': 'application/json',
9396
'Content-Type': 'application/json',
@@ -899,6 +902,129 @@ def create_ontology_from_feature_schemas(self,
899902
normalized = {'tools': tools, 'classifications': classifications}
900903
return self.create_ontology(name, normalized, media_type)
901904

905+
def delete_unused_feature_schema(self, feature_schema_id: str):
906+
"""
907+
Deletes a feature schema if it is not used by any ontologies or annotations
908+
Args:
909+
feature_schema_id (str): The id of the feature schema to delete
910+
Example:
911+
>>> client.delete_unused_feature_schema("cleabc1my012ioqvu5anyaabc")
912+
"""
913+
914+
endpoint = self.rest_endpoint + "/feature-schemas/" + urllib.parse.quote(feature_schema_id)
915+
response = requests.delete(
916+
endpoint,
917+
headers=self.headers,
918+
)
919+
920+
if response.status_code != requests.codes.no_content:
921+
raise labelbox.exceptions.LabelboxError(
922+
"Failed to delete the feature schema, message: " +
923+
str(response.json()['message']))
924+
925+
def delete_unused_ontology(self, ontology_id: str):
926+
"""
927+
Deletes an ontology if it is not used by any annotations
928+
Args:
929+
ontology_id (str): The id of the ontology to delete
930+
Example:
931+
>>> client.delete_unused_ontology("cleabc1my012ioqvu5anyaabc")
932+
"""
933+
934+
endpoint = self.rest_endpoint + "/ontologies/" + urllib.parse.quote(ontology_id)
935+
response = requests.delete(
936+
endpoint,
937+
headers=self.headers,
938+
)
939+
940+
if response.status_code != requests.codes.no_content:
941+
raise labelbox.exceptions.LabelboxError(
942+
"Failed to delete the ontology, message: " +
943+
str(response.json()['message']))
944+
945+
def update_feature_schema_title(self, feature_schema_id: str, title: str):
946+
"""
947+
Updates a title of a feature schema
948+
Args:
949+
feature_schema_id (str): The id of the feature schema to update
950+
title (str): The new title of the feature schema
951+
Returns:
952+
The updated feature schema
953+
Example:
954+
>>> client.update_feature_schema_title("cleabc1my012ioqvu5anyaabc", "New Title")
955+
"""
956+
957+
endpoint = self.rest_endpoint + "/feature-schemas/" + urllib.parse.quote(feature_schema_id) + '/definition'
958+
response = requests.patch(
959+
endpoint,
960+
headers=self.headers,
961+
json={"title": title},
962+
)
963+
964+
if response.status_code == requests.codes.ok:
965+
return self.get_feature_schema(feature_schema_id)
966+
else:
967+
raise labelbox.exceptions.LabelboxError(
968+
"Failed to update the feature schema, message: " +
969+
str(response.json()['message']))
970+
971+
def upsert_feature_schema(self, normalized: Dict):
972+
"""
973+
Upserts a feature schema
974+
Args:
975+
normalized: The feature schema to upsert
976+
Returns:
977+
The upserted feature schema
978+
Example:
979+
Insert a new feature schema
980+
>>> tool = Tool(name="tool", tool=Tool.Type.BOUNDING_BOX, color="#FF0000")
981+
>>> client.upsert_feature_schema(tool.asdict())
982+
Update an existing feature schema
983+
>>> tool = Tool(feature_schema_id="cleabc1my012ioqvu5anyaabc", name="tool", tool=Tool.Type.BOUNDING_BOX, color="#FF0000")
984+
>>> client.upsert_feature_schema(tool.asdict())
985+
"""
986+
987+
feature_schema_id = normalized.get(
988+
"featureSchemaId") or "new_feature_schema_id"
989+
endpoint = self.rest_endpoint + "/feature-schemas/" + urllib.parse.quote(
990+
feature_schema_id)
991+
response = requests.put(
992+
endpoint,
993+
headers=self.headers,
994+
json={"normalized": json.dumps(normalized)},
995+
)
996+
997+
if response.status_code == requests.codes.ok:
998+
return self.get_feature_schema(response.json()['schemaId'])
999+
else:
1000+
raise labelbox.exceptions.LabelboxError(
1001+
"Failed to upsert the feature schema, message: " +
1002+
str(response.json()['message']))
1003+
1004+
def insert_feature_schema_into_ontology(self, feature_schema_id: str, ontology_id: str, position: int):
1005+
"""
1006+
Inserts a feature schema into an ontology. If the feature schema is already in the ontology,
1007+
it will be moved to the new position.
1008+
Args:
1009+
feature_schema_id (str): The feature schema id to upsert
1010+
ontology_id (str): The id of the ontology to insert the feature schema into
1011+
position (int): The position number of the feature schema in the ontology
1012+
Example:
1013+
>>> client.insert_feature_schema_into_ontology("cleabc1my012ioqvu5anyaabc", "clefdvwl7abcgefgu3lyvcde", 2)
1014+
"""
1015+
1016+
endpoint = self.rest_endpoint + '/ontologies/' + urllib.parse.quote(
1017+
ontology_id) + "/feature-schemas/" + urllib.parse.quote(feature_schema_id)
1018+
response = requests.post(
1019+
endpoint,
1020+
headers=self.headers,
1021+
json={"position": position},
1022+
)
1023+
if response.status_code != requests.codes.created:
1024+
raise labelbox.exceptions.LabelboxError(
1025+
"Failed to insert the feature schema into the ontology, message: " +
1026+
str(response.json()['message']))
1027+
9021028
def create_ontology(self, name, normalized, media_type=None) -> Ontology:
9031029
"""
9041030
Creates an ontology from normalized data

pytest.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
[pytest]
2-
addopts = -s -vv --reruns 5 --reruns-delay 10 --durations=20
2+
addopts = -s -vv --reruns 1 --reruns-delay 10 --durations=20
33
markers =
44
slow: marks tests as slow (deselect with '-m "not slow"')

tests/integration/conftest.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,20 @@ def graphql_url(environ: str) -> str:
6565
return 'http://host.docker.internal:8080/graphql'
6666

6767

68+
def rest_url(environ: str) -> str:
69+
if environ == Environ.PROD:
70+
return 'https://api.labelbox.com/api/v1'
71+
elif environ == Environ.STAGING:
72+
return 'https://api.lb-stage.xyz/api/v1'
73+
elif environ == Environ.CUSTOM:
74+
graphql_api_endpoint = os.environ.get(
75+
'LABELBOX_TEST_REST_API_ENDPOINT')
76+
if graphql_api_endpoint is None:
77+
raise Exception(f"Missing LABELBOX_TEST_REST_API_ENDPOINT")
78+
return graphql_api_endpoint
79+
return 'http://host.docker.internal:8080/api/v1'
80+
81+
6882
def testing_api_key(environ: str) -> str:
6983
if environ == Environ.PROD:
7084
return os.environ["LABELBOX_TEST_API_KEY_PROD"]
@@ -131,7 +145,8 @@ class IntegrationClient(Client):
131145
def __init__(self, environ: str) -> None:
132146
api_url = graphql_url(environ)
133147
api_key = testing_api_key(environ)
134-
super().__init__(api_key, api_url, enable_experimental=True)
148+
rest_endpoint = rest_url(environ)
149+
super().__init__(api_key, api_url, enable_experimental=True, rest_endpoint=rest_endpoint)
135150
self.queries = []
136151

137152
def execute(self, query=None, params=None, check_naming=True, **kwargs):
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import pytest
2+
3+
from labelbox import Tool, MediaType
4+
5+
point = Tool(
6+
tool=Tool.Type.POINT,
7+
name="name",
8+
color="#ff0000",
9+
)
10+
11+
12+
def test_deletes_a_feature_schema(client):
13+
tool = client.upsert_feature_schema(point.asdict())
14+
15+
assert client.delete_unused_feature_schema(tool.normalized['featureSchemaId']) is None
16+
17+
18+
def test_cant_delete_already_deleted_feature_schema(client):
19+
tool = client.upsert_feature_schema(point.asdict())
20+
feature_schema_id = tool.normalized['featureSchemaId']
21+
22+
client.delete_unused_feature_schema(feature_schema_id) is None
23+
24+
with pytest.raises(Exception,
25+
match="Failed to delete the feature schema, message: Feature schema is already deleted"):
26+
client.delete_unused_feature_schema(feature_schema_id)
27+
28+
29+
def test_cant_delete_feature_schema_with_ontology(client):
30+
tool = client.upsert_feature_schema(point.asdict())
31+
feature_schema_id = tool.normalized['featureSchemaId']
32+
ontology = client.create_ontology_from_feature_schemas(
33+
name='ontology name',
34+
feature_schema_ids=[feature_schema_id],
35+
media_type=MediaType.Image)
36+
37+
with pytest.raises(Exception,
38+
match="Failed to delete the feature schema, message: Feature schema cannot be deleted because it is used in ontologies"):
39+
client.delete_unused_feature_schema(feature_schema_id)
40+
41+
client.delete_unused_ontology(ontology.uid)
42+
client.delete_unused_feature_schema(feature_schema_id)
43+
44+
45+
def test_throws_an_error_if_feature_schema_to_delete_doesnt_exist(client):
46+
with pytest.raises(Exception,
47+
match="Failed to delete the feature schema, message: Cannot find root schema node with feature schema id doesntexist"):
48+
client.delete_unused_feature_schema("doesntexist")
49+
50+
51+
def test_updates_a_feature_schema_title(client):
52+
tool = client.upsert_feature_schema(point.asdict())
53+
feature_schema_id = tool.normalized['featureSchemaId']
54+
new_title = "new title"
55+
updated_feature_schema = client.update_feature_schema_title(feature_schema_id, new_title)
56+
57+
assert updated_feature_schema.normalized['name'] == new_title
58+
59+
client.delete_unused_feature_schema(feature_schema_id)
60+
61+
62+
def test_throws_an_error_when_updating_a_feature_schema_with_empty_title(client):
63+
tool = client.upsert_feature_schema(point.asdict())
64+
feature_schema_id = tool.normalized['featureSchemaId']
65+
66+
with pytest.raises(Exception):
67+
client.update_feature_schema_title(feature_schema_id, "")
68+
69+
client.delete_unused_feature_schema(feature_schema_id)
70+
71+
72+
def test_throws_an_error_when_updating_not_existing_feature_schema(client):
73+
with pytest.raises(Exception):
74+
client.update_feature_schema_title("doesntexist", "new title")
75+
76+
77+
def test_creates_a_new_feature_schema(client):
78+
created_feature_schema = client.upsert_feature_schema(point.asdict())
79+
80+
assert created_feature_schema.uid is not None
81+
82+
client.delete_unused_feature_schema(created_feature_schema.normalized['featureSchemaId'])
83+
84+
85+
def test_updates_a_feature_schema(client):
86+
tool = Tool(
87+
tool=Tool.Type.POINT,
88+
name="name",
89+
color="#ff0000",
90+
)
91+
created_feature_schema = client.upsert_feature_schema(tool.asdict())
92+
tool_to_update = Tool(
93+
tool=Tool.Type.POINT,
94+
name="new name",
95+
color="#ff0000",
96+
feature_schema_id=created_feature_schema.normalized['featureSchemaId'],
97+
)
98+
updated_feature_schema = client.upsert_feature_schema(tool_to_update.asdict())
99+
100+
assert updated_feature_schema.normalized[
101+
'name'] == "new name"

0 commit comments

Comments
 (0)