diff --git a/pycaprio/core/adapters/http_adapter.py b/pycaprio/core/adapters/http_adapter.py index b479725..3f901f4 100644 --- a/pycaprio/core/adapters/http_adapter.py +++ b/pycaprio/core/adapters/http_adapter.py @@ -1,7 +1,6 @@ from typing import IO, Union from typing import List from typing import Optional - from pycaprio.core.clients.retryable_client import RetryableInceptionClient from pycaprio.core.interfaces.adapter import BaseInceptionAdapter from pycaprio.core.interfaces.client import BaseInceptionClient @@ -9,6 +8,7 @@ from pycaprio.core.mappings import AnnotationState from pycaprio.core.mappings import InceptionFormat from pycaprio.core.mappings import DocumentState +from pycaprio.core.objects.role import Role from pycaprio.core.objects.annotation import Annotation from pycaprio.core.objects.document import Document from pycaprio.core.objects.project import Project @@ -17,6 +17,7 @@ from pycaprio.core.schemas.document import DocumentSchema from pycaprio.core.schemas.project import ProjectSchema from pycaprio.core.schemas.curation import CurationSchema +from pycaprio.core.schemas.role import RoleSchema class HttpInceptionAdapter(BaseInceptionAdapter): @@ -215,6 +216,21 @@ def delete_curation(self, project: Union[Project, int], document: Union[Document self.client.delete(f"/projects/{project_id}/documents/{document_id}/curation") return True + def list_roles(self, project: Union[Project, int], user_id: str) -> List[Role]: + project_id = self._get_object_id(project) + response = self.client.get(f"/projects/{project_id}/permissions/{user_id}") + return RoleSchema().load(response.json()["body"], many=True) + + def assign_roles(self, project: Union[Project, int], user_id: str, roles: List[str]) -> List[Role]: + project_id = self._get_object_id(project) + response = self.client.post(f"/projects/{project_id}/permissions/{user_id}", data={"roles": roles}) + return RoleSchema().load(response.json()["body"], many=True) + + def delete_roles(self, project: Union[Project, int], user_id: str, roles: List[str]) -> List[Role]: + project_id = self._get_object_id(project) + response = self.client.delete(f"/projects/{project_id}/permissions/{user_id}", data={"roles": roles}) + return RoleSchema().load(response.json()["body"], many=True) + @staticmethod def _get_object_id(o: Union[int, Project, Document, Annotation]) -> int: object_id_mappings = { diff --git a/pycaprio/core/clients/retryable_client.py b/pycaprio/core/clients/retryable_client.py index d8a8259..6f9bc8e 100644 --- a/pycaprio/core/clients/retryable_client.py +++ b/pycaprio/core/clients/retryable_client.py @@ -33,8 +33,8 @@ def post( ) -> requests.Response: return self.request("post", url, data=data, form_data=form_data, files=files) - def delete(self, url: str) -> requests.Response: - return self.request("delete", url) + def delete(self, url: str, data: Optional[dict] = None) -> requests.Response: + return self.request("delete", url, data=data) def request(self, method: str, url: str, **kwargs) -> requests.Response: retries = 0 diff --git a/pycaprio/core/interfaces/adapter.py b/pycaprio/core/interfaces/adapter.py index ad94fa4..18e48a7 100644 --- a/pycaprio/core/interfaces/adapter.py +++ b/pycaprio/core/interfaces/adapter.py @@ -11,6 +11,7 @@ from pycaprio.core.objects.document import Document from pycaprio.core.objects.project import Project from pycaprio.core.objects.curation import Curation +from pycaprio.core.objects.role import RoleType class BaseInceptionAdapter(metaclass=ABCMeta): @@ -250,3 +251,32 @@ def delete_curation(self, project: Union[Project, int], document: Union[Document :param project: Project/Project id. """ pass # pragma: no cover + + @abstractmethod + def list_roles(self, project: Union[Project, int], userId: str) -> List[RoleType]: + """ + List permissions for a user in the given project (non AERO) + :param project: Project/Project id. + :param userId: Username. + """ + pass # pragma: no cover + + @abstractmethod + def assign_roles(self, project: Union[Project, int], userId: str, role: List[RoleType]) -> List[RoleType]: + """ + Assign roles to a user in the given project (non-AERO) + :param project: Project/Project id. + :param userId: Username. + :param role: List of Roles [See PermissionRoles in mappings] + """ + pass # pragma: no cover + + @abstractmethod + def delete_roles(self, project: Union[Project, int], userId: str, role: List[RoleType]) -> List[RoleType]: + """ + Delete roles to a user in the given project (non-AERO) + :param project: Project/Project id. + :param userId: Username. + :param role: List of Roles [See PermissionRoles in mappings] + """ + pass # pragma: no cover diff --git a/pycaprio/core/interfaces/client.py b/pycaprio/core/interfaces/client.py index 4abd717..35c5017 100644 --- a/pycaprio/core/interfaces/client.py +++ b/pycaprio/core/interfaces/client.py @@ -54,7 +54,7 @@ def post(self, url: str, json: dict, files: dict) -> requests.Response: pass # pragma: no cover @abstractmethod - def delete(self, url: str) -> requests.Response: + def delete(self, url: str, json: dict) -> requests.Response: """ Issues an authenticated DELETE request to Inception :param url: relative url to make request diff --git a/pycaprio/core/mappings.py b/pycaprio/core/mappings.py index 750d0a2..4475ad4 100644 --- a/pycaprio/core/mappings.py +++ b/pycaprio/core/mappings.py @@ -59,3 +59,10 @@ class DocumentState: ANNOTATION_COMPLETE = "ANNOTATION-COMPLETE" CURATION_IN_PROGRESS = "CURATION-IN-PROGRESS" CURATION_COMPLETE = "CURATION-COMPLETE" + + +class RoleType: + # https://github.com/inception-project/inception/blob/main/inception/inception-model/src/main/java/de/tudarmstadt/ukp/clarin/webanno/model/PermissionLevel.java + ANNOTATOR = "ANNOTATOR" + CURATOR = "CURATOR" + MANAGER = "MANAGER" diff --git a/pycaprio/core/objects/__init__.py b/pycaprio/core/objects/__init__.py index 22d27ee..400461d 100644 --- a/pycaprio/core/objects/__init__.py +++ b/pycaprio/core/objects/__init__.py @@ -3,3 +3,4 @@ from pycaprio.core.objects.document import Document from pycaprio.core.objects.project import Project from pycaprio.core.objects.curation import Curation +from pycaprio.core.objects.role import Role diff --git a/pycaprio/core/objects/role.py b/pycaprio/core/objects/role.py new file mode 100644 index 0000000..534194c --- /dev/null +++ b/pycaprio/core/objects/role.py @@ -0,0 +1,21 @@ +__any__ = ["Role"] + +import typing +from pycaprio.core.mappings import RoleType + + +class Role: + """ + INCEpTION's Permissions object + """ + + def __init__(self, project_id: int, userId: str, roles: typing.List[RoleType]): + self.project_id = project_id + self.userId = userId + self.roles = roles + + def __repr__(self): + return f"" + + def __str__(self): + return repr(self) diff --git a/pycaprio/core/schemas/role.py b/pycaprio/core/schemas/role.py new file mode 100644 index 0000000..e31dc4b --- /dev/null +++ b/pycaprio/core/schemas/role.py @@ -0,0 +1,24 @@ +from typing import Union +from typing import List +from pycaprio.core.interfaces.schema import BaseInceptionSchema +from pycaprio.core.objects.role import Role + +serialized_type = Union[List[dict], dict] +deserialized_type = Union[List[Role], Role] + + +class RoleSchema(BaseInceptionSchema): + """ + Schema to serialize/deserialize Roles. + Documentation is described in 'BaseInceptionSchema'. + """ + + def load(self, roles_dict: serialized_type, many: bool = False) -> deserialized_type: + if many: + return [self.load(project, many=False) for project in roles_dict] + return Role(project_id=roles_dict["project"], userId=roles_dict["user"], roles=roles_dict["role"]) + + def dump(self, roles: deserialized_type, many: bool = False) -> serialized_type: + if many: + return [self.dump(p, many=False) for p in roles] + return {"projectId": roles.project_id, "user": roles.userId, "role": roles.roles} diff --git a/pycaprio/mappings.py b/pycaprio/mappings.py index 8b86e11..379fad9 100644 --- a/pycaprio/mappings.py +++ b/pycaprio/mappings.py @@ -1,2 +1,2 @@ # flake8: noqa -from pycaprio.core.mappings import InceptionFormat, DocumentState, AnnotationState # pragma: no cover +from pycaprio.core.mappings import InceptionFormat, DocumentState, AnnotationState, RoleType # pragma: no cover