Skip to content

Commit

Permalink
Merge pull request #99 from lance132/master
Browse files Browse the repository at this point in the history
Doccano 1.7 compatibility update
  • Loading branch information
houssam7737 committed Oct 6, 2022
2 parents 6592059 + 11e139f commit 42244f4
Show file tree
Hide file tree
Showing 38 changed files with 1,419 additions and 112 deletions.
2 changes: 1 addition & 1 deletion doccano_client/beta/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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

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

### Usage

Expand Down
15 changes: 15 additions & 0 deletions doccano_client/beta/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from .annotation import AnnotationController, AnnotationsController
from .category import CategoriesController, CategoryController
from .category_type import CategoryTypeController, CategoryTypesController
from .comment import CommentController, CommentsController
from .example import (
DocumentController,
Expand All @@ -8,12 +10,19 @@
)
from .label import LabelController, LabelsController
from .project import ProjectController, ProjectsController
from .relation import RelationController, RelationsController
from .relation_type import RelationTypeController, RelationTypesController
from .span import SpanController, SpansController
from .span_type import SpanTypeController, SpanTypesController
from .text import TextController, TextsController

__all__ = [
"AnnotationController",
"AnnotationsController",
"CategoryController",
"CategoriesController",
"CategoryTypeController",
"CategoryTypesController",
"CommentController",
"CommentsController",
# TODO: Retained for backwards compatibility. Remove in v1.6.0
Expand All @@ -26,8 +35,14 @@
"LabelsController",
"ProjectController",
"ProjectsController",
"SpanController",
"SpansController",
"SpanTypeController",
"SpanTypesController",
"RelationController",
"RelationsController",
"RelationTypeController",
"RelationTypesController",
"TextController",
"TextsController",
]
90 changes: 90 additions & 0 deletions doccano_client/beta/controllers/category.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from dataclasses import asdict, dataclass, fields
from typing import Iterable

from requests import Session

from ..models import Category, Project
from ..utils.response import verbose_raise_for_status


@dataclass
class CategoryController:
"""Wraps a Category."""

id: int
category: Category
categories_url: str
client_session: Session
project: Project


class CategoriesController:
"""Controls the creation and retrieval of individual annotations for an example."""

def __init__(self, example_id: int, project: Project, example_url: str, client_session: Session):
"""Initializes a CategoriesController instance
Args:
example_id: int. The relevant example id to this annotations controller
example_url: str. Url of the parent example
project: Project. The project model of the annotations, which is needed to query
for the type of annotation used by the project.
client_session: requests.session. The current session passed from client to models
"""
self.example_id = example_id
self.project = project
self._example_url = example_url
self.client_session = client_session

@property
def categories_url(self) -> str:
"""Return an api url for annotations list of a example"""
return f"{self._example_url}/categories"

def all(self) -> Iterable[CategoryController]:
"""Return a sequence of CategoryControllers.
Yields:
CategoryController: The next category controller.
"""
response = self.client_session.get(self.categories_url)
verbose_raise_for_status(response)
category_dicts = response.json()
category_obj_fields = set(category_field.name for category_field in fields(Category))

for category_dict in category_dicts:
# Sanitize category_dict before converting to Example
sanitized_category_dict = {
category_key: category_dict[category_key] for category_key in category_obj_fields
}

yield CategoryController(
category=Category(**sanitized_category_dict),
project=self.project,
id=category_dict["id"],
categories_url=self.categories_url,
client_session=self.client_session,
)

def create(self, category: Category) -> CategoryController:
"""Create a new category, return the generated controller
Args:
category: Category. Automatically assigns session variables.
Returns:
CategoryController. The CategoryController now wrapping around the newly created category.
"""
category_json = asdict(category)

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

return CategoryController(
category=category,
project=self.project,
id=response_id,
categories_url=self.categories_url,
client_session=self.client_session,
)
92 changes: 92 additions & 0 deletions doccano_client/beta/controllers/category_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
from dataclasses import asdict, dataclass, fields
from typing import Iterable

from requests import Session

from ..models.category_type import LABEL_COLOR_CYCLE, CategoryType
from ..utils.response import verbose_raise_for_status

COLOR_CYCLE_RANGE = len(LABEL_COLOR_CYCLE)


@dataclass
class CategoryTypeController:
"""Wraps a CategoryType with fields used for interacting directly with Doccano client"""

category_type: CategoryType
id: int
category_types_url: str
client_session: Session

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


class CategoryTypesController:
"""Controls the creation and retrieval of individual CategoryTypeControllers for an assigned project"""

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

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

def all(self) -> Iterable[CategoryTypeController]:
"""Return a sequence of all category-types for a given controller, which maps to a project
Yields:
CategoryTypeController: The next category type controller.
"""
response = self.client_session.get(self.category_types_url)
verbose_raise_for_status(response)
category_type_dicts = response.json()
category_type_object_fields = set(category_type_field.name for category_type_field in fields(CategoryType))

for category_type_dict in category_type_dicts:
# Sanitize category_type_dict before converting to CategoryType
sanitized_category_type_dict = {
category_type_key: category_type_dict[category_type_key]
for category_type_key in category_type_object_fields
}

yield CategoryTypeController(
category_type=CategoryType(**sanitized_category_type_dict),
id=category_type_dict["id"],
category_types_url=self.category_types_url,
client_session=self.client_session,
)

def create(self, category_type: CategoryType) -> CategoryTypeController:
"""Create new category_type for Doccano project, assign session variables to category_type, return the id"""
category_type_json = asdict(category_type)

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

return CategoryTypeController(
category_type=category_type,
id=response_id,
category_types_url=self.category_types_url,
client_session=self.client_session,
)

def update(self, category_type_controllers: Iterable[CategoryTypeController]) -> None:
"""Updates the given category_types in the remote project"""
for category_type_controller in category_type_controllers:
category_type_json = asdict(category_type_controller.category_type)
category_type_json = {
category_type_key: category_type_value
for category_type_key, category_type_value in category_type_json.items()
if category_type_value is not None
}
category_type_json["id"] = category_type_controller.id

response = self.client_session.put(category_type_controller.category_type_url, json=category_type_json)
verbose_raise_for_status(response)
57 changes: 38 additions & 19 deletions doccano_client/beta/controllers/comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,30 +47,49 @@ def __init__(self, parent_url: str, client_session: Session) -> None:

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

def all(self) -> Iterable[CommentController]:
"""Return a sequence of Comments for a given controller, which maps to an object
Yields:
CommentController: The next comment controller.
A while loop is used because not all comments are returned at once, and additional
comments must be retrieved by calling the next url in the django response.
"""
response = self.client_session.get(self.comments_url)
verbose_raise_for_status(response)
comment_dicts = response.json()
comment_obj_fields = set(comment_field.name for comment_field in fields(Comment))

for comment_dict in comment_dicts:
# Sanitize comment_dict before converting to Comment
sanitized_comment_dict = {comment_key: comment_dict[comment_key] for comment_key in comment_obj_fields}

yield CommentController(
comment=Comment(**sanitized_comment_dict),
username=comment_dict["username"],
created_at=comment_dict["created_at"],
example=comment_dict["example"],
id=comment_dict["id"],
comments_url=self.comments_url,
client_session=self.client_session,
)

while True:
verbose_raise_for_status(response)
comment_dicts = response.json()
comment_obj_fields = set(comment_field.name for comment_field in fields(Comment))

for comment_dict in comment_dicts["results"]:
# Sanitize comment_dict before converting to Comment
sanitized_comment_dict = {comment_key: comment_dict[comment_key] for comment_key in comment_obj_fields}

yield CommentController(
comment=Comment(**sanitized_comment_dict),
username=comment_dict["username"],
created_at=comment_dict["created_at"],
example=comment_dict["example"],
id=comment_dict["id"],
comments_url=self.comments_url,
client_session=self.client_session,
)

if comment_dicts["next"] is None:
break
else:
response = self.client_session.get(comment_dicts["next"])
15 changes: 13 additions & 2 deletions doccano_client/beta/controllers/example.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
from ..models.examples import Example
from ..models.projects import Project
from ..utils.response import verbose_raise_for_status
from .category import CategoriesController
from .comment import CommentsController
from .relation import RelationsController
from .span import SpansController
from .text import TextsController

EXAMPLES_PER_PAGE_LIMIT = 10

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

@property
def categories(self) -> CategoriesController:
"""Return an CategoriesController mapped to this example"""
return CategoriesController(self.id, self.project, self.example_url, self.client_session)

@property
def spans(self) -> SpansController:
"""Return an SpanController mapped to this example"""
Expand All @@ -43,6 +50,11 @@ def relations(self) -> RelationsController:
"""Return an RelationController mapped to this example"""
return RelationsController(self.id, self.project, self.example_url, self.client_session)

@property
def texts(self) -> TextsController:
"""Return an TextsController mapped to this example"""
return TextsController(self.id, self.project, self.example_url, self.client_session)


class ExamplesController:
"""Controls the creation and retrieval of individual ExampleControllers for a project"""
Expand Down Expand Up @@ -126,8 +138,7 @@ def create(self, example: Example) -> ExampleController:
"""Upload new example for Doccano project, return the generated controller
Args:
example: Example. The only fields that will be uploaded are text, annnotations,
and meta.
example: Example. Automatically assigns session variables.
Returns:
ExampleController. The ExampleController now wrapping around the newly created example
Expand Down
11 changes: 10 additions & 1 deletion doccano_client/beta/controllers/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from ..models.projects import Project
from ..utils.response import verbose_raise_for_status
from .category_type import CategoryTypesController
from .comment import CommentsController
from .example import DocumentsController, ExamplesController
from .label import LabelsController
Expand Down Expand Up @@ -46,6 +47,11 @@ def comments(self) -> CommentsController:
"""Return a CommentsController mapped to this project"""
return CommentsController(self.project_url, self.client_session)

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

@property
def span_types(self) -> SpanTypesController:
"""Return a SpanTypesController mapped to this project"""
Expand Down Expand Up @@ -134,7 +140,10 @@ def all(self) -> Iterable[ProjectController]:

for project_dict in project_dicts["results"]:
# Sanitize project_dict before converting to Project
sanitized_project_dict = {proj_key: project_dict[proj_key] for proj_key in project_obj_fields}
sanitized_project_dict = {
proj_key: (project_dict[proj_key] if proj_key in project_dict else False)
for proj_key in project_obj_fields
}

yield ProjectController(
project=Project(**sanitized_project_dict),
Expand Down

0 comments on commit 42244f4

Please sign in to comment.