Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add MemberController #139

Merged
merged 9 commits into from
Jan 10, 2023
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
3 changes: 3 additions & 0 deletions doccano_client/beta/controllers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
ExamplesController,
)
from .label import LabelController, LabelsController
from .member import MemberController, MembersController
from .project import ProjectController, ProjectsController
from .relation import RelationController, RelationsController
from .relation_type import RelationTypeController, RelationTypesController
Expand All @@ -33,6 +34,8 @@
"ExamplesController",
"LabelController",
"LabelsController",
"MemberController",
"MembersController",
"ProjectController",
"ProjectsController",
"SpanController",
Expand Down
85 changes: 85 additions & 0 deletions doccano_client/beta/controllers/member.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from dataclasses import asdict, dataclass, fields
from typing import Iterable

from requests import Session

from ..models.members import Member
from ..utils.response import verbose_raise_for_status


@dataclass
class MemberController:
"""Wraps a Member with fields used for interacting directly with Doccano client"""

id: int
member: Member
members_url: str
client_session: Session

@property
def member_url(self) -> str:
"""Return an api url for this member"""
return f"{self.members_url}/{self.id}"


class MembersController:
"""Controls the assignment and retrieval of MemberControllers for a project"""

def __init__(self, project_url: str, client_session: Session) -> None:
"""Initializes a MemberController instance"""
self._project_url = project_url
self.client_session = client_session

@property
def members_url(self) -> str:
"""Return an api url for members list"""
return f"{self._project_url}/members"

def all(self) -> Iterable[MemberController]:
"""Return a sequence of all members for a given controller, which maps to a project

Yields:
MemberController: The next member controller.
"""
response = self.client_session.get(self.members_url)
verbose_raise_for_status(response)
member_dicts = response.json()
member_object_fields = set(member_field.name for member_field in fields(Member))

for member_dict in member_dicts:
# Sanitize member_dict before converting to Member
sanitized_member_dict = {member_key: member_dict[member_key] for member_key in member_object_fields}

yield MemberController(
member=Member(**sanitized_member_dict),
id=member_dict["id"],
members_url=self.members_url,
client_session=self.client_session,
)

def create(self, member: Member) -> MemberController:
"""Create new member for Doccano project, assign session variables to member, return the id"""
member_json = asdict(member)

response = self.client_session.post(self.members_url, json=member_json)
verbose_raise_for_status(response)
response_id = response.json()["id"]

return MemberController(
member=member,
id=response_id,
members_url=self.members_url,
client_session=self.client_session,
)

def update(self, member_controllers: Iterable[MemberController]) -> None:
"""Updates the given members in the remote project"""
for member_controller in member_controllers:
member_json = asdict(member_controller.member)
member_json = {
member_key: member_value for member_key, member_value in member_json.items() if member_value is not None
}
member_json["id"] = member_controller.id

response = self.client_session.put(member_controller.member_url, json=member_json)
verbose_raise_for_status(response)
6 changes: 6 additions & 0 deletions doccano_client/beta/controllers/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .comment import CommentsController
from .example import DocumentsController, ExamplesController
from .label import LabelsController
from .member import MembersController
from .relation_type import RelationTypesController
from .span_type import SpanTypesController

Expand Down Expand Up @@ -47,6 +48,11 @@ def comments(self) -> CommentsController:
"""Return a CommentsController mapped to this project"""
return CommentsController(self.project_url, self.client_session)

@property
def members(self) -> MembersController:
"""Return a MembersController mapped to this project"""
return MembersController(self.project_url, self.client_session)

@property
def category_types(self) -> CategoryTypesController:
"""Return a CategoryTypesController mapped to this project"""
Expand Down
2 changes: 2 additions & 0 deletions doccano_client/beta/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .comments import Comment
from .examples import Document, Example
from .labels import LABEL_COLOR_CYCLE, Label
from .members import Member
from .projects import Project, ProjectTypes
from .relation import Relation
from .relation_type import RelationType
Expand All @@ -28,6 +29,7 @@
"Example",
"Label",
"LABEL_COLOR_CYCLE",
"Member",
"ProjectTypes",
"Project",
"Relation",
Expand Down
9 changes: 9 additions & 0 deletions doccano_client/beta/models/members.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from dataclasses import dataclass


@dataclass
class Member:
"""Contains the data and operations relevant to a Member on a Doccano project"""

user: int
role: int
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import re

import responses

from . import projects

members_get_json = [
{
"id": 6,
"user": 1,
"role": 1,
"username": "user_a",
"rolename": "project_admin",
},
{
"id": 7,
"user": 2,
"role": 2,
"username": "user_b",
"rolename": "annotator",
},
]

member_create_json = {
"id": 8,
"user": 3,
"role": 3,
"username": "user_c",
"rolename": "annotation_approver",
}

members_regex = f".*/v1/projects/{projects.valid_project_ids_regex_insert}/members"

members_get_empty_response = responses.Response(method="GET", url=re.compile(members_regex), json=[], status=200)

members_get_response = responses.Response(
method="GET", url=re.compile(members_regex), json=members_get_json, status=200
)

member_create_response = projects_get_updated_response = responses.Response(
method="POST",
url=re.compile(members_regex),
json=member_create_json,
status=201,
)

member_update_response = responses.Response(
method="PUT",
url=re.compile(rf"{members_regex}/\d+"),
# The json here in practice is way more complicated, but we don't need to test or use the
# response outside of the status code, so it is moot for testing.
json={"status": "accepted"},
status=200,
)
97 changes: 97 additions & 0 deletions doccano_client/beta/tests/controllers/test_member.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
from unittest import TestCase

import responses
from requests import Session

from ...controllers import MemberController, MembersController
from ...models import Member
from ...utils.response import DoccanoAPIError
from .mock_api_responses import bad
from .mock_api_responses import members as mocks


class MemberControllerTest(TestCase):
def setUp(self):
self.member = Member(user=1, role=1)
self.member_controller = MemberController(
id=43,
member=self.member,
members_url="http://my_members_url",
client_session=Session(),
)

def test_urls(self):
self.assertEqual(self.member_controller.member_url, "http://my_members_url/43")


class MembersControllerTest(TestCase):
def setUp(self) -> None:
self.member_a = Member(user=3, role=3)
self.member_controller_a = MemberController(
id=43,
member=self.member_a,
members_url="http://my_members_url",
client_session=Session(),
)
self.members_controller = MembersController(
project_url="http://my_members_url/v1/projects/23",
client_session=Session(),
)

def test_controller_urls(self):
self.assertEqual(self.members_controller.members_url, "http://my_members_url/v1/projects/23/members")

@responses.activate
def test_all_with_no_members(self):
responses.add(mocks.members_get_empty_response)
member_controllers = self.members_controller.all()
self.assertEqual(len(list(member_controllers)), 0)

@responses.activate
def test_all(self):
responses.add(mocks.members_get_response)
member_controllers = self.members_controller.all()

total_members = 0
expected_member_id_dict = {member_json["id"]: member_json for member_json in mocks.members_get_json}
for member_controller in member_controllers:
self.assertIn(member_controller.id, expected_member_id_dict)
self.assertEqual(member_controller.member.user, expected_member_id_dict[member_controller.id]["user"])
self.assertEqual(member_controller.member.role, expected_member_id_dict[member_controller.id]["role"])
self.assertIs(member_controller.client_session, self.members_controller.client_session)
total_members += 1

self.assertEqual(total_members, len(mocks.members_get_json))

@responses.activate
def test_all_with_bad_response(self):
responses.add(bad.bad_get_response)
with self.assertRaises(DoccanoAPIError):
list(self.members_controller.all())

@responses.activate
def test_create(self):
responses.add(mocks.member_create_response)
member_a_controller = self.members_controller.create(self.member_a)

self.assertEqual(member_a_controller.id, mocks.member_create_json["id"])
self.assertEqual(member_a_controller.member.user, mocks.member_create_json["user"])

@responses.activate
def test_create_with_bad_response(self):
responses.add(bad.bad_post_response)
with self.assertRaises(DoccanoAPIError):
list(self.members_controller.create(self.member_a))

@responses.activate
def test_update(self):
responses.add(mocks.members_get_response)
responses.add(mocks.member_update_response)
member_controllers = self.members_controller.all()
self.members_controller.update(member_controllers)

@responses.activate
def test_update_with_bad_response(self):
responses.add(bad.bad_put_response)
with self.assertRaises(DoccanoAPIError):
list(self.members_controller.update([self.member_controller_a]))