Skip to content

Commit e38658e

Browse files
authored
Merge pull request #730 from superannotateai/editor_template_context
added SDK new function get_editor_context
2 parents ce3c84f + acd535c commit e38658e

File tree

9 files changed

+141
-9
lines changed

9 files changed

+141
-9
lines changed

docs/source/api_reference/api_project.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ Projects
2626
.. automethod:: superannotate.SAClient.get_project_steps
2727
.. automethod:: superannotate.SAClient.set_project_workflow
2828
.. automethod:: superannotate.SAClient.get_project_workflow
29+
.. automethod:: superannotate.SAClient.get_component_config

src/superannotate/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import sys
44

55

6-
__version__ = "4.4.28"
6+
__version__ = "4.4.29dev1"
77

88
os.environ.update({"sa_version": __version__})
99
sys.path.append(os.path.split(os.path.realpath(__file__))[0])

src/superannotate/lib/app/interface/sdk_interface.py

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import logging
66
import os
77
import sys
8+
import typing
89
import warnings
910
from pathlib import Path
1011
from typing import Any
@@ -277,6 +278,55 @@ def get_team_metadata(self):
277278
response = self.controller.get_team()
278279
return TeamSerializer(response.data).serialize()
279280

281+
def get_component_config(self, project: Union[NotEmptyStr, int], component_id: str):
282+
"""
283+
Retrieves the configuration for a given project and component ID.
284+
285+
:param project: The identifier of the project, which can be a string or an integer representing the project ID.
286+
:type project: Union[str, int]
287+
288+
:param component_id: The ID of the component for which the context is to be retrieved.
289+
:type component_id: str
290+
291+
:return: The context associated with the `webComponent`.
292+
:rtype: Any
293+
294+
:raises AppException: If the project type is not `MULTIMODAL` or no `webComponent` context is found.
295+
"""
296+
297+
def retrieve_context(
298+
component_data: List[dict], component_pk: str
299+
) -> Tuple[bool, typing.Any]:
300+
for component in component_data:
301+
if (
302+
component["type"] == "webComponent"
303+
and component["id"] == component_pk
304+
):
305+
return True, component.get("context")
306+
if component["type"] == "group" and "children" in component:
307+
found, val = retrieve_context(component["children"], component_pk)
308+
if found:
309+
return found, val
310+
return False, None
311+
312+
project = (
313+
self.controller.get_project_by_id(project).data
314+
if isinstance(project, int)
315+
else self.controller.get_project(project)
316+
)
317+
if project.type != ProjectType.MULTIMODAL:
318+
raise AppException(
319+
"This function is only supported for Multimodal projects."
320+
)
321+
322+
editor_template = self.controller.projects.get_editor_template(project)
323+
components = editor_template.get("components", [])
324+
325+
_found, _context = retrieve_context(components, component_id)
326+
if not _found:
327+
raise AppException("No component context found for project.")
328+
return _context
329+
280330
def search_team_contributors(
281331
self,
282332
email: EmailStr = None,
@@ -2700,7 +2750,7 @@ def search_items(
27002750
]
27012751
"""
27022752
project, folder = self.controller.get_project_folder_by_path(project)
2703-
query_kwargs = {}
2753+
query_kwargs = {"include": ["assignments"]}
27042754
if name_contains:
27052755
query_kwargs["name__contains"] = name_contains
27062756
if annotation_status:
@@ -2718,7 +2768,9 @@ def search_items(
27182768
f"{project.name}{f'/{folder.name}' if not folder.is_root else ''}"
27192769
)
27202770
_items = self.controller.items.list_items(
2721-
project, folder, **query_kwargs
2771+
project,
2772+
folder,
2773+
**query_kwargs,
27222774
)
27232775
for i in _items:
27242776
i.path = path
@@ -2864,7 +2916,10 @@ def list_items(
28642916
).data
28652917
else:
28662918
folder = self.controller.get_folder(project, folder)
2867-
include = include or []
2919+
_include = {"assignments"}
2920+
if include:
2921+
_include.update(set(include))
2922+
include = list(_include)
28682923
include_custom_metadata = "custom_metadata" in include
28692924
if include_custom_metadata:
28702925
include.remove("custom_metadata")
@@ -2876,7 +2931,7 @@ def list_items(
28762931
project=project, item_ids=[i.id for i in res]
28772932
)
28782933
for i in res:
2879-
i["custom_metadata"] = item_custom_fields[i.id]
2934+
i.custom_metadata = item_custom_fields[i.id]
28802935
exclude = {"meta", "annotator_email", "qa_email"}
28812936
if include:
28822937
if "custom_metadata" not in include:

src/superannotate/lib/infrastructure/controller.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ def __init__(self, service_provider: ServiceProvider):
8888

8989

9090
class ProjectManager(BaseManager):
91+
def __init__(self, service_provider: ServiceProvider, team: TeamEntity):
92+
super().__init__(service_provider)
93+
self._team = team
94+
9195
def get_by_id(self, project_id):
9296
use_case = usecases.GetProjectByIDUseCase(
9397
project_id=project_id, service_provider=self.service_provider
@@ -239,6 +243,13 @@ def upload_priority_scores(
239243
)
240244
return use_case.execute()
241245

246+
def get_editor_template(self, project: ProjectEntity) -> dict:
247+
response = self.service_provider.projects.get_editor_template(
248+
team=self._team, project=project
249+
)
250+
response.raise_for_status()
251+
return response.data
252+
242253

243254
class AnnotationClassManager(BaseManager):
244255
@timed_lru_cache(seconds=3600)
@@ -974,7 +985,7 @@ def __init__(self, config: ConfigEntity):
974985
self._user = self.get_current_user()
975986
self._team = self.get_team().data
976987
self.annotation_classes = AnnotationClassManager(self.service_provider)
977-
self.projects = ProjectManager(self.service_provider)
988+
self.projects = ProjectManager(self.service_provider, team=self._team)
978989
self.folders = FolderManager(self.service_provider)
979990
self.items = ItemManager(self.service_provider)
980991
self.annotations = AnnotationManager(self.service_provider, config)

src/superannotate/lib/infrastructure/services/item_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515

1616
class ItemService(SuperannotateServiceProvider):
17-
MAX_URI_LENGTH = 16_000
17+
MAX_URI_LENGTH = 15_000
1818
URL_LIST = "items"
1919
URL_GET = "items/{item_id}"
2020

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"components": [
3+
{
4+
"id": "web_1",
5+
"type": "webComponent",
6+
"permissions": [],
7+
"hasTooltip": false,
8+
"exclude": true,
9+
"value": "",
10+
"code": "<!-- V11 -->\n<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>Custom Component</title>\n</head>\n<body>\n <div id=\"saContainer\"></div>\n\n <script>\n const containerEl = document.getElementById(\"saContainer\");\n let contextValue = window.SA_CONTEXT;\n if (window.IS_BUILDER && window.IS_CONFIG_MODE) {\n const contextValueEl = document.createElement(\"input\");\n contextValueEl.setAttribute(\"type\", \"text\");\n contextValueEl.value = contextValue;\n contextValueEl.setAttribute(\"id\", \"contextValue\");\n\n const setContextBtn = document.createElement(\"button\");\n setContextBtn.innerHTML = \"Set Context\";\n setContextBtn.setAttribute(\"id\", \"setContextBtn\");\n \n containerEl.appendChild(contextValueEl);\n containerEl.appendChild(setContextBtn);\n\n setContextBtn.addEventListener(\"click\", async () => {\n const value = contextValueEl.value;\n console.log(\"> > > > \", value)\n await window.SA.setContext(value);\n });\n } else {\n containerEl.innerHTML = `Contect value: ${contextValue}`;\n }\n </script>\n</body>\n\n</html>",
11+
"context": "\"12121121212\""
12+
}
13+
],
14+
"code": [
15+
[
16+
"__init__",
17+
"from typing import List, Union\n# import requests.asyncs as requests\nimport requests\nimport sa\n\nwebComponent_web_1 = ['web_1']\n\ndef before_save_hook(old_status: str, new_status: str) -> bool:\n # Your code goes here\n return\n\ndef on_saved_hook():\n # Your code goes here\n return\n\ndef before_status_change_hook(old_status: str, new_status: str) -> bool:\n # Your code goes here\n return\n\ndef on_status_changed_hook(old_status: str, new_status: str):\n # Your code goes here\n return\n\ndef post_hook():\n # Your code goes here\n return\n\ndef on_session_start():\n # Your code goes here\n return\n\ndef on_session_end():\n # Your code goes here\n return\n\ndef on_web_1_message(path: List[Union[str, int]], value):\n # The path is a list of strings and integers, the length of which is always an odd number and not less than 1.\n # The last value is the identifier of the form element and the pairs preceding it are\n # the group identifiers and the subgroup index, respectively\n # value is current value of the form element\n\n # Your code goes here\n return\n\ndef on_web_1_wcevent(path: List[Union[str, int]], value):\n # The path is a list of strings and integers, the length of which is always an odd number and not less than 1.\n # The last value is the identifier of the form element and the pairs preceding it are\n # the group identifiers and the subgroup index, respectively\n # value is current value of the form element\n\n # Your code goes here\n return\n"
18+
]
19+
],
20+
"environments": []
21+
}

tests/integration/custom_fields/test_custom_schema.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ def test_upload_delete_custom_values_search_items(self):
101101
self.PROJECT_NAME, name_contains=item_name, include_custom_metadata=True
102102
)
103103
assert data[0]["custom_metadata"] == payload
104+
data = sa.list_items(
105+
self.PROJECT_NAME, came__contains=item_name, include=["custom_metadata"]
106+
)
107+
assert data[0]["custom_metadata"] == payload
104108
sa.delete_custom_values(self.PROJECT_NAME, [{item_name: ["test"]}])
105109
data = sa.search_items(
106110
self.PROJECT_NAME, name_contains=item_name, include_custom_metadata=True
@@ -115,6 +119,7 @@ def test_search_items(self):
115119
sa.upload_custom_values(self.PROJECT_NAME, [{item_name: payload}] * 10000)
116120
items = sa.search_items(self.PROJECT_NAME, include_custom_metadata=True)
117121
assert items[0]["custom_metadata"] == payload
122+
items = sa.list_items(self.PROJECT_NAME, include=["custom_metadata"])
118123

119124
def test_search_items_without_custom_metadata(self):
120125
item_name = "test"

tests/integration/items/test_item_context.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,42 @@ def test_overwrite_false(self):
8585
# test from folder by project and folder ids as tuple and item id
8686
item = sa.search_items(f"{self.PROJECT_NAME}/folder", "dummy")[0]
8787
self._base_test((self._project["id"], folder["id"]), item["id"])
88+
89+
90+
class TestEditorContext(BaseTestCase):
91+
PROJECT_NAME = "TestEditorContext"
92+
PROJECT_TYPE = "Multimodal"
93+
PROJECT_DESCRIPTION = "DESCRIPTION"
94+
COMPONENT_ID = "web_1"
95+
EDITOR_TEMPLATE_PATH = os.path.join(
96+
Path(__file__).parent.parent.parent,
97+
"data_set/sample_llm_editor_context/form.json",
98+
)
99+
100+
def setUp(self, *args, **kwargs):
101+
102+
self._project = sa.create_project(
103+
self.PROJECT_NAME,
104+
self.PROJECT_DESCRIPTION,
105+
self.PROJECT_TYPE,
106+
settings=[{"attribute": "TemplateState", "value": 1}],
107+
)
108+
team = sa.controller.team
109+
project = sa.controller.get_project(self.PROJECT_NAME)
110+
time.sleep(10)
111+
with open(self.EDITOR_TEMPLATE_PATH) as f:
112+
res = sa.controller.service_provider.projects.attach_editor_template(
113+
team, project, template=json.load(f)
114+
)
115+
assert res.ok
116+
...
117+
118+
def tearDown(self) -> None:
119+
try:
120+
sa.delete_project(self.PROJECT_NAME)
121+
except Exception:
122+
...
123+
124+
def test_(self):
125+
val = sa.get_editor_context(self.PROJECT_NAME, self.COMPONENT_ID)
126+
assert val == "12121121212"

tests/integration/test_video.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,13 @@ def test_video_upload_from_folder(self):
6060
res = sa.upload_videos_from_folder_to_project(
6161
self.PROJECT_NAME, self.folder_path, target_fps=1
6262
)
63-
assert res == [
63+
assert set(res) == {
6464
"video_001.jpg",
6565
"video_002.jpg",
6666
"video_004.jpg",
6767
"video_005.jpg",
6868
"video_003.jpg",
69-
]
69+
}
7070
sa.create_folder(self.PROJECT_NAME, self.TEST_FOLDER_NAME)
7171
sa.upload_videos_from_folder_to_project(
7272
f"{self.PROJECT_NAME}/{self.TEST_FOLDER_NAME}",

0 commit comments

Comments
 (0)