Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/source/api_reference/api_project.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ Projects
.. automethod:: superannotate.SAClient.get_project_steps
.. automethod:: superannotate.SAClient.set_project_workflow
.. automethod:: superannotate.SAClient.get_project_workflow
.. automethod:: superannotate.SAClient.get_component_config
2 changes: 1 addition & 1 deletion src/superannotate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import sys


__version__ = "4.4.28"
__version__ = "4.4.29dev1"

os.environ.update({"sa_version": __version__})
sys.path.append(os.path.split(os.path.realpath(__file__))[0])
Expand Down
63 changes: 59 additions & 4 deletions src/superannotate/lib/app/interface/sdk_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import logging
import os
import sys
import typing
import warnings
from pathlib import Path
from typing import Any
Expand Down Expand Up @@ -277,6 +278,55 @@ def get_team_metadata(self):
response = self.controller.get_team()
return TeamSerializer(response.data).serialize()

def get_component_config(self, project: Union[NotEmptyStr, int], component_id: str):
"""
Retrieves the configuration for a given project and component ID.

:param project: The identifier of the project, which can be a string or an integer representing the project ID.
:type project: Union[str, int]

:param component_id: The ID of the component for which the context is to be retrieved.
:type component_id: str

:return: The context associated with the `webComponent`.
:rtype: Any

:raises AppException: If the project type is not `MULTIMODAL` or no `webComponent` context is found.
"""

def retrieve_context(
component_data: List[dict], component_pk: str
) -> Tuple[bool, typing.Any]:
for component in component_data:
if (
component["type"] == "webComponent"
and component["id"] == component_pk
):
return True, component.get("context")
if component["type"] == "group" and "children" in component:
found, val = retrieve_context(component["children"], component_pk)
if found:
return found, val
return False, None

project = (
self.controller.get_project_by_id(project).data
if isinstance(project, int)
else self.controller.get_project(project)
)
if project.type != ProjectType.MULTIMODAL:
raise AppException(
"This function is only supported for Multimodal projects."
)

editor_template = self.controller.projects.get_editor_template(project)
components = editor_template.get("components", [])

_found, _context = retrieve_context(components, component_id)
if not _found:
raise AppException("No component context found for project.")
return _context

def search_team_contributors(
self,
email: EmailStr = None,
Expand Down Expand Up @@ -2700,7 +2750,7 @@ def search_items(
]
"""
project, folder = self.controller.get_project_folder_by_path(project)
query_kwargs = {}
query_kwargs = {"include": ["assignments"]}
if name_contains:
query_kwargs["name__contains"] = name_contains
if annotation_status:
Expand All @@ -2718,7 +2768,9 @@ def search_items(
f"{project.name}{f'/{folder.name}' if not folder.is_root else ''}"
)
_items = self.controller.items.list_items(
project, folder, **query_kwargs
project,
folder,
**query_kwargs,
)
for i in _items:
i.path = path
Expand Down Expand Up @@ -2864,7 +2916,10 @@ def list_items(
).data
else:
folder = self.controller.get_folder(project, folder)
include = include or []
_include = {"assignments"}
if include:
_include.update(set(include))
include = list(_include)
include_custom_metadata = "custom_metadata" in include
if include_custom_metadata:
include.remove("custom_metadata")
Expand All @@ -2876,7 +2931,7 @@ def list_items(
project=project, item_ids=[i.id for i in res]
)
for i in res:
i["custom_metadata"] = item_custom_fields[i.id]
i.custom_metadata = item_custom_fields[i.id]
exclude = {"meta", "annotator_email", "qa_email"}
if include:
if "custom_metadata" not in include:
Expand Down
13 changes: 12 additions & 1 deletion src/superannotate/lib/infrastructure/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ def __init__(self, service_provider: ServiceProvider):


class ProjectManager(BaseManager):
def __init__(self, service_provider: ServiceProvider, team: TeamEntity):
super().__init__(service_provider)
self._team = team

def get_by_id(self, project_id):
use_case = usecases.GetProjectByIDUseCase(
project_id=project_id, service_provider=self.service_provider
Expand Down Expand Up @@ -239,6 +243,13 @@ def upload_priority_scores(
)
return use_case.execute()

def get_editor_template(self, project: ProjectEntity) -> dict:
response = self.service_provider.projects.get_editor_template(
team=self._team, project=project
)
response.raise_for_status()
return response.data


class AnnotationClassManager(BaseManager):
@timed_lru_cache(seconds=3600)
Expand Down Expand Up @@ -974,7 +985,7 @@ def __init__(self, config: ConfigEntity):
self._user = self.get_current_user()
self._team = self.get_team().data
self.annotation_classes = AnnotationClassManager(self.service_provider)
self.projects = ProjectManager(self.service_provider)
self.projects = ProjectManager(self.service_provider, team=self._team)
self.folders = FolderManager(self.service_provider)
self.items = ItemManager(self.service_provider)
self.annotations = AnnotationManager(self.service_provider, config)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@


class ItemService(SuperannotateServiceProvider):
MAX_URI_LENGTH = 16_000
MAX_URI_LENGTH = 15_000
URL_LIST = "items"
URL_GET = "items/{item_id}"

Expand Down
21 changes: 21 additions & 0 deletions tests/data_set/sample_llm_editor_context/form.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"components": [
{
"id": "web_1",
"type": "webComponent",
"permissions": [],
"hasTooltip": false,
"exclude": true,
"value": "",
"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>",
"context": "\"12121121212\""
}
],
"code": [
[
"__init__",
"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"
]
],
"environments": []
}
5 changes: 5 additions & 0 deletions tests/integration/custom_fields/test_custom_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ def test_upload_delete_custom_values_search_items(self):
self.PROJECT_NAME, name_contains=item_name, include_custom_metadata=True
)
assert data[0]["custom_metadata"] == payload
data = sa.list_items(
self.PROJECT_NAME, came__contains=item_name, include=["custom_metadata"]
)
assert data[0]["custom_metadata"] == payload
sa.delete_custom_values(self.PROJECT_NAME, [{item_name: ["test"]}])
data = sa.search_items(
self.PROJECT_NAME, name_contains=item_name, include_custom_metadata=True
Expand All @@ -115,6 +119,7 @@ def test_search_items(self):
sa.upload_custom_values(self.PROJECT_NAME, [{item_name: payload}] * 10000)
items = sa.search_items(self.PROJECT_NAME, include_custom_metadata=True)
assert items[0]["custom_metadata"] == payload
items = sa.list_items(self.PROJECT_NAME, include=["custom_metadata"])

def test_search_items_without_custom_metadata(self):
item_name = "test"
Expand Down
39 changes: 39 additions & 0 deletions tests/integration/items/test_item_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,42 @@ def test_overwrite_false(self):
# test from folder by project and folder ids as tuple and item id
item = sa.search_items(f"{self.PROJECT_NAME}/folder", "dummy")[0]
self._base_test((self._project["id"], folder["id"]), item["id"])


class TestEditorContext(BaseTestCase):
PROJECT_NAME = "TestEditorContext"
PROJECT_TYPE = "Multimodal"
PROJECT_DESCRIPTION = "DESCRIPTION"
COMPONENT_ID = "web_1"
EDITOR_TEMPLATE_PATH = os.path.join(
Path(__file__).parent.parent.parent,
"data_set/sample_llm_editor_context/form.json",
)

def setUp(self, *args, **kwargs):

self._project = sa.create_project(
self.PROJECT_NAME,
self.PROJECT_DESCRIPTION,
self.PROJECT_TYPE,
settings=[{"attribute": "TemplateState", "value": 1}],
)
team = sa.controller.team
project = sa.controller.get_project(self.PROJECT_NAME)
time.sleep(10)
with open(self.EDITOR_TEMPLATE_PATH) as f:
res = sa.controller.service_provider.projects.attach_editor_template(
team, project, template=json.load(f)
)
assert res.ok
...

def tearDown(self) -> None:
try:
sa.delete_project(self.PROJECT_NAME)
except Exception:
...

def test_(self):
val = sa.get_editor_context(self.PROJECT_NAME, self.COMPONENT_ID)
assert val == "12121121212"
4 changes: 2 additions & 2 deletions tests/integration/test_video.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,13 @@ def test_video_upload_from_folder(self):
res = sa.upload_videos_from_folder_to_project(
self.PROJECT_NAME, self.folder_path, target_fps=1
)
assert res == [
assert set(res) == {
"video_001.jpg",
"video_002.jpg",
"video_004.jpg",
"video_005.jpg",
"video_003.jpg",
]
}
sa.create_folder(self.PROJECT_NAME, self.TEST_FOLDER_NAME)
sa.upload_videos_from_folder_to_project(
f"{self.PROJECT_NAME}/{self.TEST_FOLDER_NAME}",
Expand Down