Skip to content

Commit 42244f4

Browse files
authored
Merge pull request #99 from lance132/master
Doccano 1.7 compatibility update
2 parents 6592059 + 11e139f commit 42244f4

38 files changed

+1419
-112
lines changed

doccano_client/beta/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
A beta version of a new design for the Doccano client, featuring more Pythonic interaction, as well as thorough testing and documentation.
44

5-
Currently tested for compatibility against Doccano v1.5.0-1.5.5.
5+
Currently tested for compatibility against Doccano v1.7.0
66

77
### Usage
88

doccano_client/beta/controllers/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from .annotation import AnnotationController, AnnotationsController
2+
from .category import CategoriesController, CategoryController
3+
from .category_type import CategoryTypeController, CategoryTypesController
24
from .comment import CommentController, CommentsController
35
from .example import (
46
DocumentController,
@@ -8,12 +10,19 @@
810
)
911
from .label import LabelController, LabelsController
1012
from .project import ProjectController, ProjectsController
13+
from .relation import RelationController, RelationsController
1114
from .relation_type import RelationTypeController, RelationTypesController
15+
from .span import SpanController, SpansController
1216
from .span_type import SpanTypeController, SpanTypesController
17+
from .text import TextController, TextsController
1318

1419
__all__ = [
1520
"AnnotationController",
1621
"AnnotationsController",
22+
"CategoryController",
23+
"CategoriesController",
24+
"CategoryTypeController",
25+
"CategoryTypesController",
1726
"CommentController",
1827
"CommentsController",
1928
# TODO: Retained for backwards compatibility. Remove in v1.6.0
@@ -26,8 +35,14 @@
2635
"LabelsController",
2736
"ProjectController",
2837
"ProjectsController",
38+
"SpanController",
39+
"SpansController",
2940
"SpanTypeController",
3041
"SpanTypesController",
42+
"RelationController",
43+
"RelationsController",
3144
"RelationTypeController",
3245
"RelationTypesController",
46+
"TextController",
47+
"TextsController",
3348
]
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
from dataclasses import asdict, dataclass, fields
2+
from typing import Iterable
3+
4+
from requests import Session
5+
6+
from ..models import Category, Project
7+
from ..utils.response import verbose_raise_for_status
8+
9+
10+
@dataclass
11+
class CategoryController:
12+
"""Wraps a Category."""
13+
14+
id: int
15+
category: Category
16+
categories_url: str
17+
client_session: Session
18+
project: Project
19+
20+
21+
class CategoriesController:
22+
"""Controls the creation and retrieval of individual annotations for an example."""
23+
24+
def __init__(self, example_id: int, project: Project, example_url: str, client_session: Session):
25+
"""Initializes a CategoriesController instance
26+
27+
Args:
28+
example_id: int. The relevant example id to this annotations controller
29+
example_url: str. Url of the parent example
30+
project: Project. The project model of the annotations, which is needed to query
31+
for the type of annotation used by the project.
32+
client_session: requests.session. The current session passed from client to models
33+
"""
34+
self.example_id = example_id
35+
self.project = project
36+
self._example_url = example_url
37+
self.client_session = client_session
38+
39+
@property
40+
def categories_url(self) -> str:
41+
"""Return an api url for annotations list of a example"""
42+
return f"{self._example_url}/categories"
43+
44+
def all(self) -> Iterable[CategoryController]:
45+
"""Return a sequence of CategoryControllers.
46+
47+
Yields:
48+
CategoryController: The next category controller.
49+
"""
50+
response = self.client_session.get(self.categories_url)
51+
verbose_raise_for_status(response)
52+
category_dicts = response.json()
53+
category_obj_fields = set(category_field.name for category_field in fields(Category))
54+
55+
for category_dict in category_dicts:
56+
# Sanitize category_dict before converting to Example
57+
sanitized_category_dict = {
58+
category_key: category_dict[category_key] for category_key in category_obj_fields
59+
}
60+
61+
yield CategoryController(
62+
category=Category(**sanitized_category_dict),
63+
project=self.project,
64+
id=category_dict["id"],
65+
categories_url=self.categories_url,
66+
client_session=self.client_session,
67+
)
68+
69+
def create(self, category: Category) -> CategoryController:
70+
"""Create a new category, return the generated controller
71+
72+
Args:
73+
category: Category. Automatically assigns session variables.
74+
75+
Returns:
76+
CategoryController. The CategoryController now wrapping around the newly created category.
77+
"""
78+
category_json = asdict(category)
79+
80+
response = self.client_session.post(self.categories_url, json=category_json)
81+
verbose_raise_for_status(response)
82+
response_id = response.json()["id"]
83+
84+
return CategoryController(
85+
category=category,
86+
project=self.project,
87+
id=response_id,
88+
categories_url=self.categories_url,
89+
client_session=self.client_session,
90+
)
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
from dataclasses import asdict, dataclass, fields
2+
from typing import Iterable
3+
4+
from requests import Session
5+
6+
from ..models.category_type import LABEL_COLOR_CYCLE, CategoryType
7+
from ..utils.response import verbose_raise_for_status
8+
9+
COLOR_CYCLE_RANGE = len(LABEL_COLOR_CYCLE)
10+
11+
12+
@dataclass
13+
class CategoryTypeController:
14+
"""Wraps a CategoryType with fields used for interacting directly with Doccano client"""
15+
16+
category_type: CategoryType
17+
id: int
18+
category_types_url: str
19+
client_session: Session
20+
21+
@property
22+
def category_type_url(self) -> str:
23+
"""Return an api url for this category_type"""
24+
return f"{self.category_types_url}/{self.id}"
25+
26+
27+
class CategoryTypesController:
28+
"""Controls the creation and retrieval of individual CategoryTypeControllers for an assigned project"""
29+
30+
def __init__(self, project_url: str, client_session: Session) -> None:
31+
"""Initializes a CategoryTypeController instance"""
32+
self._project_url = project_url
33+
self.client_session = client_session
34+
35+
@property
36+
def category_types_url(self) -> str:
37+
"""Return an api url for category_types list"""
38+
return f"{self._project_url}/category-types"
39+
40+
def all(self) -> Iterable[CategoryTypeController]:
41+
"""Return a sequence of all category-types for a given controller, which maps to a project
42+
43+
Yields:
44+
CategoryTypeController: The next category type controller.
45+
"""
46+
response = self.client_session.get(self.category_types_url)
47+
verbose_raise_for_status(response)
48+
category_type_dicts = response.json()
49+
category_type_object_fields = set(category_type_field.name for category_type_field in fields(CategoryType))
50+
51+
for category_type_dict in category_type_dicts:
52+
# Sanitize category_type_dict before converting to CategoryType
53+
sanitized_category_type_dict = {
54+
category_type_key: category_type_dict[category_type_key]
55+
for category_type_key in category_type_object_fields
56+
}
57+
58+
yield CategoryTypeController(
59+
category_type=CategoryType(**sanitized_category_type_dict),
60+
id=category_type_dict["id"],
61+
category_types_url=self.category_types_url,
62+
client_session=self.client_session,
63+
)
64+
65+
def create(self, category_type: CategoryType) -> CategoryTypeController:
66+
"""Create new category_type for Doccano project, assign session variables to category_type, return the id"""
67+
category_type_json = asdict(category_type)
68+
69+
response = self.client_session.post(self.category_types_url, json=category_type_json)
70+
verbose_raise_for_status(response)
71+
response_id = response.json()["id"]
72+
73+
return CategoryTypeController(
74+
category_type=category_type,
75+
id=response_id,
76+
category_types_url=self.category_types_url,
77+
client_session=self.client_session,
78+
)
79+
80+
def update(self, category_type_controllers: Iterable[CategoryTypeController]) -> None:
81+
"""Updates the given category_types in the remote project"""
82+
for category_type_controller in category_type_controllers:
83+
category_type_json = asdict(category_type_controller.category_type)
84+
category_type_json = {
85+
category_type_key: category_type_value
86+
for category_type_key, category_type_value in category_type_json.items()
87+
if category_type_value is not None
88+
}
89+
category_type_json["id"] = category_type_controller.id
90+
91+
response = self.client_session.put(category_type_controller.category_type_url, json=category_type_json)
92+
verbose_raise_for_status(response)

doccano_client/beta/controllers/comment.py

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -47,30 +47,49 @@ def __init__(self, parent_url: str, client_session: Session) -> None:
4747

4848
@property
4949
def comments_url(self) -> str:
50-
"""Return an api url for comments list of an object"""
51-
return f"{self._parent_url}/comments"
50+
"""Return an api url for comments list of an object
51+
52+
Either a project or example can have a comments property. If the instantiating url
53+
is an example, then we parse the example id in order to form the comments list url.
54+
"""
55+
if "/examples" in self._parent_url:
56+
base_url = self._parent_url[: self._parent_url.rindex("/examples")]
57+
example_id = self._parent_url[self._parent_url.rindex("/examples") + 10 :]
58+
return f"{base_url}/comments?example={example_id}"
59+
else:
60+
return f"{self._parent_url}/comments"
5261

5362
def all(self) -> Iterable[CommentController]:
5463
"""Return a sequence of Comments for a given controller, which maps to an object
5564
5665
Yields:
5766
CommentController: The next comment controller.
67+
68+
A while loop is used because not all comments are returned at once, and additional
69+
comments must be retrieved by calling the next url in the django response.
5870
"""
5971
response = self.client_session.get(self.comments_url)
60-
verbose_raise_for_status(response)
61-
comment_dicts = response.json()
62-
comment_obj_fields = set(comment_field.name for comment_field in fields(Comment))
63-
64-
for comment_dict in comment_dicts:
65-
# Sanitize comment_dict before converting to Comment
66-
sanitized_comment_dict = {comment_key: comment_dict[comment_key] for comment_key in comment_obj_fields}
67-
68-
yield CommentController(
69-
comment=Comment(**sanitized_comment_dict),
70-
username=comment_dict["username"],
71-
created_at=comment_dict["created_at"],
72-
example=comment_dict["example"],
73-
id=comment_dict["id"],
74-
comments_url=self.comments_url,
75-
client_session=self.client_session,
76-
)
72+
73+
while True:
74+
verbose_raise_for_status(response)
75+
comment_dicts = response.json()
76+
comment_obj_fields = set(comment_field.name for comment_field in fields(Comment))
77+
78+
for comment_dict in comment_dicts["results"]:
79+
# Sanitize comment_dict before converting to Comment
80+
sanitized_comment_dict = {comment_key: comment_dict[comment_key] for comment_key in comment_obj_fields}
81+
82+
yield CommentController(
83+
comment=Comment(**sanitized_comment_dict),
84+
username=comment_dict["username"],
85+
created_at=comment_dict["created_at"],
86+
example=comment_dict["example"],
87+
id=comment_dict["id"],
88+
comments_url=self.comments_url,
89+
client_session=self.client_session,
90+
)
91+
92+
if comment_dicts["next"] is None:
93+
break
94+
else:
95+
response = self.client_session.get(comment_dicts["next"])

doccano_client/beta/controllers/example.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
from ..models.examples import Example
77
from ..models.projects import Project
88
from ..utils.response import verbose_raise_for_status
9+
from .category import CategoriesController
910
from .comment import CommentsController
1011
from .relation import RelationsController
1112
from .span import SpansController
13+
from .text import TextsController
1214

1315
EXAMPLES_PER_PAGE_LIMIT = 10
1416

@@ -33,6 +35,11 @@ def comments(self) -> CommentsController:
3335
"""Return a CommentsController mapped to this example"""
3436
return CommentsController(self.example_url, self.client_session)
3537

38+
@property
39+
def categories(self) -> CategoriesController:
40+
"""Return an CategoriesController mapped to this example"""
41+
return CategoriesController(self.id, self.project, self.example_url, self.client_session)
42+
3643
@property
3744
def spans(self) -> SpansController:
3845
"""Return an SpanController mapped to this example"""
@@ -43,6 +50,11 @@ def relations(self) -> RelationsController:
4350
"""Return an RelationController mapped to this example"""
4451
return RelationsController(self.id, self.project, self.example_url, self.client_session)
4552

53+
@property
54+
def texts(self) -> TextsController:
55+
"""Return an TextsController mapped to this example"""
56+
return TextsController(self.id, self.project, self.example_url, self.client_session)
57+
4658

4759
class ExamplesController:
4860
"""Controls the creation and retrieval of individual ExampleControllers for a project"""
@@ -126,8 +138,7 @@ def create(self, example: Example) -> ExampleController:
126138
"""Upload new example for Doccano project, return the generated controller
127139
128140
Args:
129-
example: Example. The only fields that will be uploaded are text, annnotations,
130-
and meta.
141+
example: Example. Automatically assigns session variables.
131142
132143
Returns:
133144
ExampleController. The ExampleController now wrapping around the newly created example

doccano_client/beta/controllers/project.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from ..models.projects import Project
88
from ..utils.response import verbose_raise_for_status
9+
from .category_type import CategoryTypesController
910
from .comment import CommentsController
1011
from .example import DocumentsController, ExamplesController
1112
from .label import LabelsController
@@ -46,6 +47,11 @@ def comments(self) -> CommentsController:
4647
"""Return a CommentsController mapped to this project"""
4748
return CommentsController(self.project_url, self.client_session)
4849

50+
@property
51+
def category_types(self) -> CategoryTypesController:
52+
"""Return a CategoryTypesController mapped to this project"""
53+
return CategoryTypesController(self.project_url, self.client_session)
54+
4955
@property
5056
def span_types(self) -> SpanTypesController:
5157
"""Return a SpanTypesController mapped to this project"""
@@ -134,7 +140,10 @@ def all(self) -> Iterable[ProjectController]:
134140

135141
for project_dict in project_dicts["results"]:
136142
# Sanitize project_dict before converting to Project
137-
sanitized_project_dict = {proj_key: project_dict[proj_key] for proj_key in project_obj_fields}
143+
sanitized_project_dict = {
144+
proj_key: (project_dict[proj_key] if proj_key in project_dict else False)
145+
for proj_key in project_obj_fields
146+
}
138147

139148
yield ProjectController(
140149
project=Project(**sanitized_project_dict),

0 commit comments

Comments
 (0)