-
Notifications
You must be signed in to change notification settings - Fork 64
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #139 from lance132/master
Add MemberController
- Loading branch information
Showing
7 changed files
with
256 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
54 changes: 54 additions & 0 deletions
54
doccano_client/beta/tests/controllers/mock_api_responses/members.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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])) |