diff --git a/labelbox/schema/project.py b/labelbox/schema/project.py index b8afa1ec9..5715d0c57 100644 --- a/labelbox/schema/project.py +++ b/labelbox/schema/project.py @@ -173,6 +173,69 @@ def export_labels(self, timeout_seconds=60): self.uid) time.sleep(sleep_time) + def upsert_instructions(self, instructions_file: str): + """ + * Uploads instructions to the UI. Running more than once will replace the instructions + + Args: + instructions_file (str): Path to a local file. + * Must be either a pdf, text, or html file. + + Raises: + ValueError: + * project must be setup + * instructions file must end with one of ".text", ".txt", ".pdf", ".html" + """ + + if self.setup_complete is None: + raise ValueError( + "Cannot attach instructions to a project that has not been set up." + ) + + frontend = self.labeling_frontend() + frontendId = frontend.uid + + if frontend.name != "Editor": + logger.warn( + f"This function has only been tested to work with the Editor front end. Found %s", + frontend.name) + + supported_instruction_formats = (".text", ".txt", ".pdf", ".html") + if not instructions_file.endswith(supported_instruction_formats): + raise ValueError( + f"instructions_file must end with one of {supported_instruction_formats}. Found {instructions_file}" + ) + + lfo = list(self.labeling_frontend_options())[-1] + instructions_url = self.client.upload_file(instructions_file) + customization_options = json.loads(lfo.customization_options) + customization_options['projectInstructions'] = instructions_url + option_id = lfo.uid + + self.client.execute( + """mutation UpdateFrontendWithExistingOptionsPyApi ( + $frontendId: ID!, + $optionsId: ID!, + $name: String!, + $description: String!, + $customizationOptions: String! + ) { + updateLabelingFrontend( + where: {id: $frontendId}, + data: {name: $name, description: $description} + ) {id} + updateLabelingFrontendOptions( + where: {id: $optionsId}, + data: {customizationOptions: $customizationOptions} + ) {id} + }""", { + "frontendId": frontendId, + "optionsId": option_id, + "name": frontend.name, + "description": "Video, image, and text annotation", + "customizationOptions": json.dumps(customization_options) + }) + def labeler_performance(self): """ Returns the labeler performances for this Project. diff --git a/tests/integration/test_project.py b/tests/integration/test_project.py index aadead361..6297443f2 100644 --- a/tests/integration/test_project.py +++ b/tests/integration/test_project.py @@ -1,6 +1,8 @@ import pytest +import json +import requests -from labelbox import Project +from labelbox import Project, LabelingFrontend from labelbox.exceptions import InvalidQueryError @@ -64,3 +66,29 @@ def test_extend_reservations(project): assert project.extend_reservations("ReviewQueue") == 0 with pytest.raises(InvalidQueryError): project.extend_reservations("InvalidQueueType") + + +def test_attach_instructions(client, project): + with pytest.raises(ValueError) as execinfo: + project.upsert_instructions('/tmp/instructions.txt') + assert str( + execinfo.value + ) == "Cannot attach instructions to a project that has not been set up." + + editor = list( + client.get_labeling_frontends( + where=LabelingFrontend.name == "editor"))[0] + empty_ontology = {"tools": [], "classifications": []} + project.setup(editor, empty_ontology) + + with open('/tmp/instructions.txt', 'w') as file: + file.write("some instructions...") + + project.upsert_instructions('/tmp/instructions.txt') + assert json.loads( + list(project.labeling_frontend_options()) + [-1].customization_options).get('projectInstructions') is not None + + with pytest.raises(ValueError) as execinfo: + project.upsert_instructions('/tmp/file.invalid_file_extension') + assert "instructions_file must end with one of" in str(execinfo.value)