From 5e5a98e978907888f1b8e7b475c04411eb3b3b9b Mon Sep 17 00:00:00 2001 From: Evan Blaudy Date: Mon, 8 Jan 2024 15:09:13 +0100 Subject: [PATCH 1/4] [requirements] upgrade black, pre-commit & python-socketio --- setup.cfg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index c1890dd6..4b80b8ed 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,7 +30,7 @@ classifiers = zip_safe = False packages = find: install_requires = - python-socketio[client]==5.10.0; python_version != '2.7' + python-socketio[client]==5.11.0; python_version != '2.7' requests>=2.25.1 [options.packages.find] @@ -47,5 +47,5 @@ test = requests_mock lint = - black==23.9.1; python_version != '2.7' - pre-commit==3.5.0; python_version >= '3.8' + black==23.12.1; python_version >= '3.8' + pre-commit==3.6.0; python_version >= '3.9' From 4c11761e5bd18e11025d2503c0d7d9d14f516224 Mon Sep 17 00:00:00 2001 From: Evan Blaudy Date: Tue, 9 Jan 2024 02:06:57 +0100 Subject: [PATCH 2/4] [concepts] add new functions + tests --- gazu/__init__.py | 1 + gazu/concept.py | 149 ++++++++++++++++++++++++++++++++++++++++ gazu/entity.py | 13 ++++ gazu/task.py | 34 +++++++++ tests/test_concept.py | 155 ++++++++++++++++++++++++++++++++++++++++++ tests/test_shot.py | 28 ++++---- tests/test_task.py | 58 ++++++++++++---- 7 files changed, 408 insertions(+), 30 deletions(-) create mode 100644 gazu/concept.py create mode 100644 tests/test_concept.py diff --git a/gazu/__init__.py b/gazu/__init__.py index e9698548..13f28ba2 100644 --- a/gazu/__init__.py +++ b/gazu/__init__.py @@ -20,6 +20,7 @@ from . import task from . import user from . import playlist +from . import concept from .exception import ( AuthFailedException, diff --git a/gazu/concept.py b/gazu/concept.py new file mode 100644 index 00000000..03ebd12a --- /dev/null +++ b/gazu/concept.py @@ -0,0 +1,149 @@ +from . import client as raw + +from .sorting import sort_by_name +from .cache import cache +from .helpers import ( + normalize_model_parameter, + normalize_list_of_models_for_links, +) + +default = raw.default_client + + +@cache +def all_concepts(client=default): + """ + Returns: + list: All concepts from database. + """ + concepts = raw.fetch_all("concepts", client=client) + return sort_by_name(concepts) + + +@cache +def all_concepts_for_project(project, client=default): + """ + Args: + project (str / dict): The project dict or the project ID. + + Returns: + list: Concepts from database or for given project. + """ + project = normalize_model_parameter(project) + concepts = raw.fetch_all( + "projects/%s/concepts" % project["id"], client=client + ) + return sort_by_name(concepts) + + +@cache +def all_previews_for_concept(concept, client=default): + """ + Args: + concept (str / dict): The concept dict or the concept ID. + + Returns: + list: Previews from database for given concept. + """ + concept = normalize_model_parameter(concept) + return raw.fetch_all( + "concepts/%s/preview-files" % concept["id"], client=client + ) + + +def remove_concept(concept, force=False, client=default): + """ + Remove given concept from database. + + Args: + concept (dict / str): Concept to remove. + """ + concept = normalize_model_parameter(concept) + path = "data/concepts/%s" % concept["id"] + params = {} + if force: + params = {"force": "true"} + return raw.delete(path, params, client=client) + + +@cache +def get_concept(concept_id, client=default): + """ + Args: + concept_id (str): ID of claimed concept. + + Returns: + dict: Concept corresponding to given concept ID. + """ + return raw.fetch_one("concepts", concept_id, client=client) + + +@cache +def get_concept_by_name(project, concept_name, client=default): + """ + Args: + project (str / dict): The project dict or the project ID. + concept_name (str): Name of claimed concept. + + Returns: + dict: Concept corresponding to given name and project. + """ + project = normalize_model_parameter(project) + return raw.fetch_first( + "concepts", + {"project_id": project["id"], "name": concept_name}, + client=client, + ) + + +def new_concept( + project, + name, + description=None, + data={}, + entity_concept_links=[], + client=default, +): + """ + Create a concept for given project. Allow to set metadata too. + + Args: + project (str / dict): The project dict or the project ID. + name (str): The name of the concept to create. + data (dict): Free field to set metadata of any kind. + entity_concept_links (list): List of entities to tag. + Returns: + Created concept. + """ + project = normalize_model_parameter(project) + data = { + "name": name, + "data": data, + "entity_concept_links": normalize_list_of_models_for_links( + entity_concept_links + ), + } + + if description is not None: + data["description"] = description + + concept = get_concept_by_name(project, name, client=client) + if concept is None: + path = "data/projects/%s/concepts" % project["id"] + return raw.post(path, data, client=client) + else: + return concept + + +def update_concept(concept, client=default): + """ + Save given concept data into the API. Metadata are fully replaced by the ones + set on given concept. + + Args: + concept (dict): The concept dict to update. + + Returns: + dict: Updated concept. + """ + return raw.put("data/entities/%s" % concept["id"], concept, client=client) diff --git a/gazu/entity.py b/gazu/entity.py index 84ae081f..9c214c21 100644 --- a/gazu/entity.py +++ b/gazu/entity.py @@ -127,3 +127,16 @@ def remove_entity(entity, force=False, client=default): if force: params = {"force": "true"} return raw.delete(path, params, client=client) + + +def all_entities_with_tasks_linked_to_entity(entity, client=default): + """ + Args: + entity (dict): Entity to get linked entities. + Returns: + list: Retrieve all entities linked to given entity. + """ + entity = normalize_model_parameter(entity) + return raw.fetch_all( + "entities/%s/entities-linked/with-tasks" % entity["id"], client=client + ) diff --git a/gazu/task.py b/gazu/task.py index 7629bc46..4198ae2b 100644 --- a/gazu/task.py +++ b/gazu/task.py @@ -83,6 +83,25 @@ def all_tasks_for_shot(shot, relations=False, client=default): return sort_by_name(tasks) +@cache +def all_tasks_for_concept(concept, relations=False, client=default): + """ + Args: + concept (str / dict): The concept dict or the concept ID. + + Returns: + list: Tasks linked to given concept. + """ + concept = normalize_model_parameter(concept) + params = {} + if relations: + params = {"relations": "true"} + tasks = raw.fetch_all( + "concepts/%s/tasks" % concept["id"], params, client=client + ) + return sort_by_name(tasks) + + @cache def all_tasks_for_edit(edit, relations=False, client=default): """ @@ -272,6 +291,21 @@ def all_task_types_for_shot(shot, client=default): return sort_by_name(task_types) +@cache +def all_task_types_for_concept(concept, client=default): + """ + Args: + concept (str / dict): The concept dict or the concept ID. + + Returns + list: Task types of task linked to given concept. + """ + concept = normalize_model_parameter(concept) + path = "concepts/%s/task-types" % concept["id"] + task_types = raw.fetch_all(path, client=client) + return sort_by_name(task_types) + + @cache def all_task_types_for_asset(asset, client=default): """ diff --git a/tests/test_concept.py b/tests/test_concept.py new file mode 100644 index 00000000..a5d46187 --- /dev/null +++ b/tests/test_concept.py @@ -0,0 +1,155 @@ +import unittest +import requests_mock + +import gazu.client +import gazu.concept + +from utils import fakeid, mock_route + + +class ConceptTestCase(unittest.TestCase): + def test_all_concepts(self): + with requests_mock.mock() as mock: + mock_route( + mock, + "GET", + "data/concepts", + text=[{"name": "Concept 01", "project_id": "project-01"}], + ) + concepts = gazu.concept.all_concepts() + self.assertEqual(len(concepts), 1) + concept_instance = concepts[0] + self.assertEqual(concept_instance["name"], "Concept 01") + self.assertEqual(concept_instance["project_id"], "project-01") + + def test_all_concepts_for_project(self): + with requests_mock.mock() as mock: + mock_route( + mock, + "GET", + "data/projects/project-01/concepts", + text=[{"name": "Concept 01", "project_id": "project-01"}], + ) + project = {"id": "project-01"} + concepts = gazu.concept.all_concepts_for_project(project) + self.assertEqual(len(concepts), 1) + concept_instance = concepts[0] + self.assertEqual(concept_instance["name"], "Concept 01") + self.assertEqual(concept_instance["project_id"], "project-01") + + def test_all_previews_for_concept(self): + with requests_mock.mock() as mock: + mock_route( + mock, + "GET", + "data/concepts/%s/preview-files" % fakeid("concept-1"), + text=[ + {"id": fakeid("preview-1"), "name": "preview-1"}, + {"id": fakeid("preview-2"), "name": "preview-2"}, + ], + ) + + previews = gazu.concept.all_previews_for_concept( + fakeid("concept-1") + ) + self.assertEqual(len(previews), 2) + self.assertEqual(previews[0]["id"], fakeid("preview-1")) + self.assertEqual(previews[1]["id"], fakeid("preview-2")) + + def test_remove_concept(self): + with requests_mock.mock() as mock: + mock_route( + mock, "DELETE", "data/concepts/concept-01", status_code=204 + ) + concept = {"id": "concept-01", "name": "S02"} + gazu.concept.remove_concept(concept) + mock_route( + mock, + "DELETE", + "data/concepts/concept-01?force=true", + status_code=204, + ) + concept = {"id": "concept-01", "name": "S02"} + gazu.concept.remove_concept(concept, True) + + def test_get_concept(self): + with requests_mock.mock() as mock: + mock_route( + mock, + "GET", + "data/concepts/concept-01", + text={"name": "Concept 01", "project_id": "project-01"}, + ) + self.assertEqual( + gazu.concept.get_concept("concept-01")["name"], "Concept 01" + ) + + def test_get_concept_by_name(self): + with requests_mock.mock() as mock: + mock_route( + mock, + "GET", + "data/concepts?project_id=project-01&name=Concept01", + text=[{"name": "Concept01", "project_id": "project-01"}], + ) + project = {"id": "project-01"} + concept = gazu.concept.get_concept_by_name(project, "Concept01") + self.assertEqual(concept["name"], "Concept01") + + def test_update_concept(self): + with requests_mock.mock() as mock: + mock_route( + mock, + "PUT", + "data/entities/concept-01", + text={"id": "concept-01", "project_id": "project-01"}, + ) + concept = {"id": "concept-01", "name": "S02"} + concept = gazu.concept.update_concept(concept) + self.assertEqual(concept["id"], "concept-01") + + def test_new_concept(self): + with requests_mock.mock() as mock: + result = { + "id": fakeid("concept-1"), + "project_id": fakeid("project-1"), + "description": "test description", + } + mock_route( + mock, + "GET", + "data/concepts?project_id=%s&name=Concept 01" + % (fakeid("project-1")), + text=[], + ) + mock_route( + mock, + "POST", + "data/projects/%s/concepts" % (fakeid("project-1")), + text=result, + ) + concept = gazu.concept.new_concept( + fakeid("project-1"), + "Concept 01", + description="test description", + ) + self.assertEqual(concept, result) + + with requests_mock.mock() as mock: + result = { + "id": fakeid("concept-1"), + "project_id": fakeid("project-1"), + } + mock_route( + mock, + "GET", + "data/concepts?project_id=%s&name=Concept 01" + % fakeid("project-1"), + text=[result], + ) + + concept = gazu.concept.new_concept( + fakeid("project-1"), + "Concept 01", + ) + self.assertEqual(concept, result) diff --git a/tests/test_shot.py b/tests/test_shot.py index d52b31b2..4afc03d1 100644 --- a/tests/test_shot.py +++ b/tests/test_shot.py @@ -12,11 +12,11 @@ class ShotTestCase(unittest.TestCase): def test_all_shots_for_project(self): with requests_mock.mock() as mock: - mock.get( - gazu.client.get_full_url("data/projects/project-01/shots"), - text=json.dumps( - [{"name": "Shot 01", "project_id": "project-01"}] - ), + mock_route( + mock, + "GET", + "data/projects/project-01/shots", + text=[{"name": "Shot 01", "project_id": "project-01"}], ) project = {"id": "project-01"} shots = gazu.shot.all_shots_for_project(project) @@ -498,16 +498,14 @@ def test_all_sequences_for_episode(self): def test_all_previews_for_shot(self): with requests_mock.mock() as mock: - mock.get( - gazu.client.get_full_url( - "data/shots/%s/preview-files" % fakeid("shot-1") - ), - text=json.dumps( - [ - {"id": fakeid("preview-1"), "name": "preview-1"}, - {"id": fakeid("preview-2"), "name": "preview-2"}, - ] - ), + mock_route( + mock, + "GET", + "data/shots/%s/preview-files" % fakeid("shot-1"), + text=[ + {"id": fakeid("preview-1"), "name": "preview-1"}, + {"id": fakeid("preview-2"), "name": "preview-2"}, + ], ) previews = gazu.shot.all_previews_for_shot(fakeid("shot-1")) diff --git a/tests/test_task.py b/tests/test_task.py index 45d7d684..ac1d52ad 100644 --- a/tests/test_task.py +++ b/tests/test_task.py @@ -8,24 +8,21 @@ TaskMustBeADictException, ) import gazu.task -import datetime from utils import fakeid, mock_route, add_verify_file_callback class TaskTestCase(unittest.TestCase): - def test_all_for_shot(self): + def test_all_tasks_for_shot(self): with requests_mock.mock() as mock: - mock.get( - gazu.client.get_full_url( - "data/shots/shot-01/tasks?relations=true" - ), - text=json.dumps( - [ - {"id": 1, "name": "Master Compositing"}, - {"id": 2, "name": "Master Animation"}, - ] - ), + mock_route( + mock, + "GET", + "data/shots/shot-01/tasks?relations=true", + text=[ + {"id": 1, "name": "Master Compositing"}, + {"id": 2, "name": "Master Animation"}, + ], ) shot = {"id": "shot-01"} @@ -33,7 +30,24 @@ def test_all_for_shot(self): task = tasks[0] self.assertEqual(task["name"], "Master Animation") - def test_all_for_sequence(self): + def test_all_tasks_for_concept(self): + with requests_mock.mock() as mock: + mock_route( + mock, + "GET", + "data/concepts/concept-01/tasks?relations=true", + text=[ + {"id": 1, "name": "Master Compositing"}, + {"id": 2, "name": "Master Animation"}, + ], + ) + + concept = {"id": "concept-01"} + tasks = gazu.task.all_tasks_for_concept(concept, True) + task = tasks[0] + self.assertEqual(task["name"], "Master Animation") + + def test_all_tasks_for_sequence(self): with requests_mock.mock() as mock: mock.get( gazu.client.get_full_url( @@ -52,7 +66,7 @@ def test_all_for_sequence(self): task = tasks[0] self.assertEqual(task["name"], "Master Animation") - def test_all_for_asset(self): + def test_all_tasks_for_asset(self): with requests_mock.mock() as mock: mock.get( gazu.client.get_full_url( @@ -71,7 +85,7 @@ def test_all_for_asset(self): task = tasks[0] self.assertEqual(task["name"], "Master Modeling") - def test_all_for_episode(self): + def test_all_tasks_for_episode(self): with requests_mock.mock() as mock: mock.get( gazu.client.get_full_url( @@ -109,6 +123,20 @@ def test_all_task_types_for_shot(self): task_type = task_types[0] self.assertEqual(task_type["name"], "Modeling") + def test_all_task_types_for_concept(self): + with requests_mock.mock() as mock: + mock_route( + mock, + "GET", + "data/concepts/concept-01/task-types", + text=[{"id": "task-type-01", "name": "Modeling"}], + ) + + concept = {"id": "concept-01"} + task_types = gazu.task.all_task_types_for_concept(concept) + task_type = task_types[0] + self.assertEqual(task_type["name"], "Modeling") + def test_all_task_types_for_sequence(self): with requests_mock.mock() as mock: mock_route( From 1253636d22aa35eb80c30b0b9d7360ca5b47b6dd Mon Sep 17 00:00:00 2001 From: Evan Blaudy Date: Tue, 9 Jan 2024 02:18:35 +0100 Subject: [PATCH 3/4] [qa][edits] add some missing tests --- tests/test_edit.py | 52 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/test_edit.py b/tests/test_edit.py index 4ecce4ce..db598960 100644 --- a/tests/test_edit.py +++ b/tests/test_edit.py @@ -117,6 +117,26 @@ def test_new_edit(self): ) self.assertEqual(edit, result) + with requests_mock.mock() as mock: + result = { + "id": fakeid("edit-1"), + "project_id": fakeid("project-1"), + } + mock_route( + mock, + "GET", + "data/edits/all?project_id=%s&name=Concept 01" + % fakeid("project-1"), + text=[result], + ) + + edit = gazu.edit.new_edit( + fakeid("project-1"), + "Concept 01", + episode=fakeid("episode-1"), + ) + self.assertEqual(edit, result) + def test_remove_edit(self): with requests_mock.mock() as mock: mock_route(mock, "DELETE", "data/edits/edit-01", status_code=204) @@ -163,3 +183,35 @@ def test_update_edit_data(self): data = {"metadata-1": "metadata-1"} edit = gazu.edit.update_edit_data(fakeid("edit-1"), data) self.assertEqual(edit["data"]["metadata-1"], "metadata-1") + + def test_all_edits_for_project(self): + with requests_mock.mock() as mock: + mock_route( + mock, + "GET", + "data/projects/project-01/edits", + text=[{"name": "Edit 01", "project_id": "project-01"}], + ) + project = {"id": "project-01"} + edits = gazu.edit.all_edits_for_project(project) + self.assertEqual(len(edits), 1) + edit_instance = edits[0] + self.assertEqual(edit_instance["name"], "Edit 01") + self.assertEqual(edit_instance["project_id"], "project-01") + + def test_all_previews_for_edit(self): + with requests_mock.mock() as mock: + mock_route( + mock, + "GET", + "data/edits/%s/preview-files" % fakeid("edit-1"), + text=[ + {"id": fakeid("preview-1"), "name": "preview-1"}, + {"id": fakeid("preview-2"), "name": "preview-2"}, + ], + ) + + previews = gazu.edit.all_previews_for_edit(fakeid("edit-1")) + self.assertEqual(len(previews), 2) + self.assertEqual(previews[0]["id"], fakeid("preview-1")) + self.assertEqual(previews[1]["id"], fakeid("preview-2")) From 31d1e283393790eae231465a67d57d585d3c058c Mon Sep 17 00:00:00 2001 From: Evan Blaudy Date: Tue, 9 Jan 2024 02:22:14 +0100 Subject: [PATCH 4/4] [tasks] add missing parameter for_entity for gazu.task.new_task_type() --- gazu/task.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gazu/task.py b/gazu/task.py index 4198ae2b..9bc43913 100644 --- a/gazu/task.py +++ b/gazu/task.py @@ -1112,7 +1112,7 @@ def clear_assignations(tasks, person=None, client=default): ) -def new_task_type(name, color="#000000", client=default): +def new_task_type(name, color="#000000", for_entity="Asset", client=default): """ Create a new task type with the given name. @@ -1120,11 +1120,12 @@ def new_task_type(name, color="#000000", client=default): name (str): The name of the task type color (str): The color of the task type as an hexadecimal string with # as first character. ex : #00FF00 + for_entity (str): The entity type for which the task type is created. Returns: dict: The created task type """ - data = {"name": name, "color": color} + data = {"name": name, "color": color, "for_entity": for_entity} return raw.post("data/task-types", data, client=client)