Skip to content

Commit

Permalink
Merge pull request #139 from lance132/master
Browse files Browse the repository at this point in the history
Add MemberController
  • Loading branch information
Hironsan authored Jan 10, 2023
2 parents d626bc5 + b00231e commit 738e865
Show file tree
Hide file tree
Showing 7 changed files with 256 additions and 0 deletions.
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]))

0 comments on commit 738e865

Please sign in to comment.